diff -Nru droidlysis-3.4.0/conf/kit.conf droidlysis-3.4.1/conf/kit.conf --- droidlysis-3.4.0/conf/kit.conf 2022-01-10 14:18:18.000000000 +0000 +++ droidlysis-3.4.1/conf/kit.conf 2023-01-09 14:29:51.000000000 +0000 @@ -4,6 +4,10 @@ [abtasty] pattern=com/abtasty +[accessibility] +pattern=android/view/accessibility +description=Android Accessibility API + [acra] pattern=org/acra description=Application Crash Reports for Android (ACRA) @@ -88,6 +92,10 @@ [alimama] pattern=com/adsmogo +[aliott] +pattern=com/aliott/agileplugin +description=Aliott Agile Plugin + [alohalytics] pattern=org/alohalytics @@ -137,6 +145,10 @@ pattern=com/andrognito description=Hide files or images on the device +[andromo] +pattern=com/andromo +description=Andromo - No code Android app platform based on Flutter + [anjlab] pattern=com/anjlab/android/iab/ @@ -291,6 +303,10 @@ pattern=com/cocosw/bottomsheet description=Bottom sheets library +[bouncycastle] +pattern=org/bouncycastle +description=Bouncycastle crypto provider + [bugfender] pattern=com/bugfender/sdk @@ -346,6 +362,10 @@ pattern=com/chirag/RNMail description=React Native Mail +[chromium] +pattern=org/chromium/support_lib_boundary +description=Support library boundary + [cifrasoft] pattern=com/cifrasoft @@ -523,6 +543,10 @@ pattern=com/flurry/android|com/flurry/sdk description=Flurry analytics +[flutter] +pattern=com/wisecrab/wc_flutter_share|com/pichillilorenzo/flutter_inappwebview +description=Flutter or its plugins + [fluzo] pattern=com/fluzo/sdk @@ -825,6 +849,10 @@ pattern=me/leolin/shortcutbadger description=Leolin Badge notification library +[lifecycle] +pattern=android/arch/lifecycle|androix/lifecycle +description=Android Lifecycle + [llvision] pattern=com/llvision description=UI templates for applications on Android, Smart Glasses @@ -978,6 +1006,14 @@ [picasso] pattern=com/squareup/picasso +[pingstart] +pattern=com/pingstart/adsdk +description=PingStart Monetization SDK + +[piracychecker] +pattern=com/github/javiersantos/piracychecker +description=Piracy Checker using Google Play Licensing + [piwik] pattern=org/piwik/sdk description=Matomo Analytics (formerly piwik) @@ -1221,9 +1257,15 @@ [zxcvbn] pattern=com/nulabinc/zxcvbn description=JavaScript password strength generator + +[zxing] +pattern=com/google/zxing +description=Zebra Crossing 1D/2D barcode image processing + [atinternet] pattern=com/atinternet + [aarki] pattern=com/aarki @@ -1365,6 +1407,10 @@ [kontakt] pattern=com/kontakt/sdk/android +[kotlin] +pattern=kotlin/coroutines/jvm|kotlin/internal|kotlinx/coroutines +description=Kotlin framework + [leanplum] pattern=com/leanplum @@ -1499,9 +1545,6 @@ pattern=com/foursquare/pilgrim description=Pilgrim by Foursquare -[pingstart] -pattern=com/pingstart/adsdk - [pinterest] pattern=com/pinterest/android/pdk @@ -1721,6 +1764,10 @@ [tenjin] pattern=com/tenjin/android/TenjinSDK +[thalitor] +pattern=com/msopentech/thali/android/toronionproxy +description=Tor Onion Proxy Library + [tinder] pattern=com/tinder/analytics description=Tinder analytics @@ -1839,6 +1886,7 @@ pattern=com/flymob/sdk [ironsource] +description=ironSource tracking API pattern=com/ironsource [mparticle] diff -Nru droidlysis-3.4.0/conf/smali.conf droidlysis-3.4.1/conf/smali.conf --- droidlysis-3.4.0/conf/smali.conf 2021-10-25 07:25:15.000000000 +0000 +++ droidlysis-3.4.1/conf/smali.conf 2022-06-20 13:30:17.000000000 +0000 @@ -86,6 +86,10 @@ pattern=Landroid/content/pm/PackageManager;->checkPermission|Landroid/content/Context;->checkPermission description=Checks for given permissions +[class_loader] +pattern=Class;->getClassLoader +description=Get class loader. Can be used for reflexion or dynamic class loading + [contacts] pattern=android/provider/ContactsContract description=Reads or lists phone contacts @@ -131,8 +135,8 @@ description=Queries the address of a DNS server [doze_mode] -pattern=REQUEST_IGNORE_BATTERY_OPTIMIZATIONS -description=Screen for controlling which apps can ignore battery optimizations +pattern=;->isIgnoringBatteryOptimizations|REQUEST_IGNORE_BATTERY_OPTIMIZATIONSREQUEST_IGNORE_BATTERY_OPTIMIZATIONS +description=Ignore battery optimizations (used to avoid running as foreground service) [email] pattern=EXTRA_EMAIL|EXTRA_SUBJECT|EXTRA_BCC|EXTRA_CC|extra\.SUBJECT|android/net/MailTo @@ -223,6 +227,10 @@ pattern=SubscriptionInfo;->getSimSlotIndex description=Get SIM slot index +[get_top_activity_component] +pattern=Landroid/app/ActivityManager\$RunningTaskInfo;->topActivity +description=Get the component of the top activity + [gps] pattern=Location;->getLatitude|Location;->getLongitude|;->getCid|;->getLac|LocationManager;->getLastKnownLocation|TelephonyManager;->getCellLocation|LocationManager;->requestLocationUpdates|TelephonyManager;->getNeighboringCellInfo description=Uses GPS location @@ -243,6 +251,7 @@ pattern=HttpGet|HttpMessage|HttpRequest|URLConnection;->openConnection description=Performs HTTP GET + [intent_chooser] pattern=Intent;->createChooser description=Uses intent chooses to ask end-user what application to use when a given event occurs (e.g which email app to use to send an email) @@ -268,7 +277,7 @@ description=Uses JSON objects [keyguard] -pattern=KeyguardManager$KeyguardLock;->|FLAG_DISMISS_KEYGUARD|android/app/admin/DevicePolicyManager;->lockNow +pattern=KeyguardManager\$KeyguardLock;->|FLAG_DISMISS_KEYGUARD|android/app/admin/DevicePolicyManager;->lockNow description=Probably tries to unlock the phone [kill_proc] @@ -359,8 +368,12 @@ pattern=android/media/AudioRecord;->startRecording description=Records audio on the phone +[record_screen] +pattern=Landroid/media/projection/MediaProjection;->createVirtualDisplay +description=Records screen + [reflection] -pattern=Class;->forName|Method;->invoke|Class;->getDeclaredMethods|Method;->setAccessible|java/lang/ClassLoader;->loadClass|Class;->getMethod +pattern=Class;->forName|Method;->invoke|Class;->getDeclaredMethods|Method;->setAccessible|java/lang/ClassLoader;->loadClass|Class;->getMethod|java/lang/reflect/Constructor;->newInstance description=Uses Java Reflection [ringer] @@ -448,7 +461,7 @@ description=Creates a random identifier. Used to identify the user. [version] -pattern=Build$VERSION;->RELEASE|Build$VERSION;->CODENAME +pattern=Build\$VERSION;->RELEASE|Build\$VERSION;->CODENAME description=Build version [vibrate] @@ -460,7 +473,7 @@ description=Probably tries to load an app [wakelock] -pattern=android/os/PowerManager$WakeLock;->acquire() +pattern=android/os/PowerManager\$WakeLock;->acquire() description=Get PowerManager WakeLock (typically used to conceal a running malware while keeping screen blank) [wallpaper] @@ -478,3 +491,4 @@ [zip] pattern=java/util/zip/ZipOutputStream|java/util/zip/ZipInputStream|java/util/zip/ZipEntry description=Zips or unzips files + diff -Nru droidlysis-3.4.0/conf/wide.conf droidlysis-3.4.1/conf/wide.conf --- droidlysis-3.4.0/conf/wide.conf 2021-08-19 09:28:51.000000000 +0000 +++ droidlysis-3.4.1/conf/wide.conf 2022-06-13 09:28:37.000000000 +0000 @@ -23,7 +23,7 @@ description=CoinHive JavaScript SDK for mining Monero [cryptocurrency] -pattern=CoinHive|crypta\.js|crypto-loot|ethereum|dogecoin|litecoin| ripple |bitcoin|ledger|blockchain|trezor +pattern=CoinHive|crypta\.js|crypto-loot|ethereum|dogecoin|litecoin|bitcoin|ledger|blockchain|trezor description=Uses cryptocurrencies [cryptoloot] diff -Nru droidlysis-3.4.0/debian/changelog droidlysis-3.4.1/debian/changelog --- droidlysis-3.4.0/debian/changelog 2023-02-06 20:24:22.000000000 +0000 +++ droidlysis-3.4.1/debian/changelog 2023-02-22 10:55:30.000000000 +0000 @@ -1,3 +1,15 @@ +droidlysis (3.4.1-1) unstable; urgency=medium + + * New upstream version 3.4.1 + + -- Hans-Christoph Steiner Wed, 22 Feb 2023 11:55:30 +0100 + +droidlysis (3.4.0-2) unstable; urgency=medium + + * fix APK detection with bookworm's libmagic version + + -- Hans-Christoph Steiner Mon, 20 Feb 2023 16:24:12 +0100 + droidlysis (3.4.0-1) unstable; urgency=medium * New upstream version 3.4.0 diff -Nru droidlysis-3.4.0/debian/control droidlysis-3.4.1/debian/control --- droidlysis-3.4.0/debian/control 2023-02-05 23:21:02.000000000 +0000 +++ droidlysis-3.4.1/debian/control 2023-02-22 10:55:30.000000000 +0000 @@ -18,10 +18,11 @@ Architecture: all Depends: ${misc:Depends}, ${python3:Depends}, + androguard, apktool, default-jre-headless, libsmali-java, - procyon-decompiler, + procyon-decompiler, Description: Property extractor for Android apps DroidLysis is a property extractor for Android apps. It automatically disassembles the Android application you provide and looks for diff -Nru droidlysis-3.4.0/debian/patches/revert-removing-androguard-as-per-issue-8.patch droidlysis-3.4.1/debian/patches/revert-removing-androguard-as-per-issue-8.patch --- droidlysis-3.4.0/debian/patches/revert-removing-androguard-as-per-issue-8.patch 1970-01-01 00:00:00.000000000 +0000 +++ droidlysis-3.4.1/debian/patches/revert-removing-androguard-as-per-issue-8.patch 2023-02-22 10:55:30.000000000 +0000 @@ -0,0 +1,56 @@ +From 519993791573190f8f749a33849fa20fcbea2989 Mon Sep 17 00:00:00 2001 +From: cryptax +Date: Tue, 21 Feb 2023 11:51:47 +0100 +Description: Debian includes androguard, so no need to remove support for it. +Subject: [PATCH 1/1] Revert "removing androguard as per issue #8" + +This reverts commit 7123ce7846f9a7e5a23717570713560f9367cf0e. +Forwarded: https://github.com/cryptax/droidlysis/issues/12 +--- + droidsample.py | 23 +++++++++++++++-------- + 1 file changed, 15 insertions(+), 8 deletions(-) + +diff --git a/droidsample.py b/droidsample.py +index 0ece41b..8d3ffb1 100644 +--- a/droidsample.py ++++ b/droidsample.py +@@ -163,6 +163,7 @@ class droidsample: + java -jar apktool.jar [-q] d file.apk outdir/apktool + if apktool failed + java -jar baksmali.jar -o outdir/smali classes.dex in file.apk ++ androaxml.py --input binary-manifest in file.apk --output outdir/AndroidManifest.text.xml + if clear unset, + unzip file.apk -d outdir/unzipped + else +@@ -309,14 +310,20 @@ class droidsample: + if self.properties.filetype == droidutil.APK: + manifest = os.path.join(self.outdir, 'AndroidManifest.xml') + if not os.access(manifest, os.R_OK) or os.path.getsize(manifest) == 0: +- logging.warning("Failed to extract binary manifest") +- """ +- we could attempt to do it with androaxml.py, but that introduces a dependancy to androguard +- just for that we could do it with ~/Android/Sdk/tools/bin/apkanalyzer manifest print package.apk, +- but that means finding apkanalyzer on the user's host + being sure they have Java 8 as the tool +- does not work with Java 11. +- it's simply not worth doing it, as this case happens very very seldom. +- """ ++ if self.verbose: ++ print("Extracting binary AndroidManifest.xml") ++ try: ++ self.ziprar.extract_one_file('AndroidManifest.xml', self.outdir) ++ except: ++ print("Failed to extract binary manifest: %s" % (sys.exc_info()[0])) ++ if os.access(manifest, os.R_OK) and os.path.getsize(manifest) > 0: ++ textmanifest = os.path.join(self.outdir, 'AndroidManifest.text.xml') ++ subprocess.call([ "/usr/bin/androaxml", "--input", manifest, \ ++ "--output", textmanifest ], \ ++ stdout=self.process_output, stderr=self.process_output) ++ if os.access(textmanifest, os.R_OK): ++ # overwrite the binary manifest with the converted text one ++ os.rename(textmanifest, manifest) + + def extract_file_properties(self): + """Extracts file size, +-- +2.39.1 + diff -Nru droidlysis-3.4.0/debian/patches/series droidlysis-3.4.1/debian/patches/series --- droidlysis-3.4.0/debian/patches/series 2020-08-27 09:31:15.000000000 +0000 +++ droidlysis-3.4.1/debian/patches/series 2023-02-22 10:55:30.000000000 +0000 @@ -1 +1,2 @@ set-path-to-debian-dependencies.patch +revert-removing-androguard-as-per-issue-8.patch diff -Nru droidlysis-3.4.0/debian/patches/set-path-to-debian-dependencies.patch droidlysis-3.4.1/debian/patches/set-path-to-debian-dependencies.patch --- droidlysis-3.4.0/debian/patches/set-path-to-debian-dependencies.patch 2023-02-06 20:24:22.000000000 +0000 +++ droidlysis-3.4.1/debian/patches/set-path-to-debian-dependencies.patch 2023-02-22 10:52:29.000000000 +0000 @@ -8,25 +8,14 @@ # ------------------------- DroidLysis Configuration file ----------------- --APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.6.0.jar") +-APKTOOL_JAR = os.path.join(os.path.expanduser("~/softs"), "apktool_2.7.0.jar") -BAKSMALI_JAR = os.path.join(os.path.expanduser("~/softs"), "baksmali-2.5.2.jar") --DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.sh") --PROCYON_JAR = os.path.join( os.path.expanduser("~/softs"), "procyon-decompiler-0.5.30.jar") +-DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.2-SNAPSHOT"), "d2j-dex2jar.sh") +-PROCYON_JAR = os.path.join(os.path.expanduser("~/softs"), "procyon-decompiler-0.5.30.jar") +APKTOOL_JAR = '/usr/share/apktool/apktool-lib.jar' +BAKSMALI_JAR = '/usr/share/java/baksmali.jar' +DEX2JAR_CMD = os.path.join(os.path.expanduser('~/.local/share/droidlysis/dex-tools-2.1-SNAPSHOT'), "d2j-dex2jar.sh") +PROCYON_JAR = '/usr/share/java/procyon-decompiler.jar' INSTALL_DIR = os.path.dirname(__file__) - SQLALCHEMY = 'sqlite:///droidlysis.db' # https://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls - KEYTOOL = os.path.join( "/usr/bin/keytool" ) ---- a/droidsample.py -+++ b/droidsample.py -@@ -332,7 +332,7 @@ - print("Failed to extract binary manifest: %s" % (sys.exc_info()[0])) - if os.access( manifest, os.R_OK) and os.path.getsize(manifest)>0: - textmanifest = os.path.join( self.outdir, 'AndroidManifest.text.xml') -- subprocess.call( [ "androaxml.py", "--input", manifest, \ -+ subprocess.call( [ "/usr/bin/androaxml", "--input", manifest, \ - "--output", textmanifest ], \ - stdout=self.process_output, stderr=self.process_output) - if os.access( textmanifest, os.R_OK ): + SQLALCHEMY = 'sqlite:///droidlysis.db' # https://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls + KEYTOOL = os.path.join("/usr/bin/keytool") diff -Nru droidlysis-3.4.0/droidconfig.py droidlysis-3.4.1/droidconfig.py --- droidlysis-3.4.0/droidconfig.py 2021-09-20 08:27:20.000000000 +0000 +++ droidlysis-3.4.1/droidconfig.py 2023-02-21 13:28:16.000000000 +0000 @@ -3,33 +3,34 @@ # ------------------------- DroidLysis Configuration file ----------------- -APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.6.0.jar") +APKTOOL_JAR = os.path.join(os.path.expanduser("~/softs"), "apktool_2.7.0.jar") BAKSMALI_JAR = os.path.join(os.path.expanduser("~/softs"), "baksmali-2.5.2.jar") -DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.sh") -PROCYON_JAR = os.path.join( os.path.expanduser("~/softs"), "procyon-decompiler-0.5.30.jar") +DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.2-SNAPSHOT"), "d2j-dex2jar.sh") +PROCYON_JAR = os.path.join(os.path.expanduser("~/softs"), "procyon-decompiler-0.5.30.jar") INSTALL_DIR = os.path.dirname(__file__) -SQLALCHEMY = 'sqlite:///droidlysis.db' # https://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls -KEYTOOL = os.path.join( "/usr/bin/keytool" ) +SQLALCHEMY = 'sqlite:///droidlysis.db' # https://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls +KEYTOOL = os.path.join("/usr/bin/keytool") # ------------------------- Property configuration files ------------------- SMALI_CONFIGFILE = os.path.join(os.path.join(INSTALL_DIR, './conf/smali.conf')) -WIDE_CONFIGFILE= os.path.join(os.path.join(INSTALL_DIR, './conf/wide.conf')) -ARM_CONFIGFILE = os.path.join(os.path.join(INSTALL_DIR, './conf/arm.conf')) -KIT_CONFIGFILE = os.path.join(os.path.join(INSTALL_DIR, './conf/kit.conf')) +WIDE_CONFIGFILE = os.path.join(os.path.join(INSTALL_DIR, './conf/wide.conf')) +ARM_CONFIGFILE = os.path.join(os.path.join(INSTALL_DIR, './conf/arm.conf')) +KIT_CONFIGFILE = os.path.join(os.path.join(INSTALL_DIR, './conf/kit.conf')) # ------------------------- Reading *.conf configuration files ----------- + class droidconfig: def __init__(self, filename, verbose=False): - assert filename != None, "Filename is invalid" - assert os.access(filename, os.R_OK) != False, "File {0} is not readable".format(filename) + assert filename is not None, "Filename is invalid" + assert os.access(filename, os.R_OK) is not False, "File {0} is not readable".format(filename) self.filename = filename self.verbose = verbose self.configparser = configparser.RawConfigParser() if self.verbose: - print( "Reading configuration file: '%s'" % (filename)) + print("Reading configuration file: '%s'" % (filename)) self.configparser.read(filename) def get_sections(self): @@ -49,34 +50,35 @@ # reads the config file and returns a list of all patterns for all sections # the patterns are concatenated with a | # throws NoSectionError, NoOptionError - allpatterns='' + allpatterns = '' for section in self.configparser.sections(): if allpatterns == '': allpatterns = self.configparser.get(section, 'pattern') else: - allpatterns= self.configparser.get(section, 'pattern') + '|' + allpatterns + allpatterns = self.configparser.get(section, 'pattern') + '|' + allpatterns return bytes(allpatterns, 'utf-8') def match_properties(self, match, properties): - ''' + """ Call this when the recursive search has been done to analyze the results and understand which properties have been spotted. - match: returned by droidutil.recursive_search. This is a dictionary + @param match: returned by droidutil.recursive_search. This is a dictionary of matching lines ordered by matching keyword (pattern) - properties: dictionary of properties where the key is the property name + @param properties: dictionary of properties where the key is the property name and the value will be False/True if set or not - + throws NoSessionError, NoOptionError - ''' + """ for section in self.configparser.sections(): pattern_list = self.configparser.get(section, 'pattern').split('|') properties[section] = False for pattern in pattern_list: - if match[pattern]: + # beware when pattern has blah\$binz, the matching key is blah$binz + if match[pattern.replace('\\', '')]: if self.verbose: - print( "Setting properties[%s] = True (matches %s)" % (section, pattern)) + print("Setting properties[%s] = True (matches %s)" % (section, pattern)) properties[section] = True break diff -Nru droidlysis-3.4.0/droidcountry.py droidlysis-3.4.1/droidcountry.py --- droidlysis-3.4.0/droidcountry.py 2021-08-19 09:28:51.000000000 +0000 +++ droidlysis-3.4.1/droidcountry.py 2023-02-21 13:28:16.000000000 +0000 @@ -5,272 +5,275 @@ __status__ = "Alpha" __license__ = "MIT License" """ -country = { 'af' : 0, - 'ax':1, - 'al':2, - 'dz':3, - 'as':4, - 'ad':5, - 'ao':6, - 'ai':7, - 'aq':8, - 'ag':9, - 'ar':10, -'am':11, -'aw':12, -'au':13, -'at':14, -'az':15, -'bs':16, -'bh':17, -'bd':18, -'bb':19, -'by':20, -'be':21, -'bz':22, -'bj':23, -'bm':24, -'bt':25, -'bo':26, -'bq':27, -'ba':28, -'bw':29, -'bv':30, -'br':31, -'io':32, -'bn':33, -'bg':34, -'bf':35, -'bi':36, -'kh':37, -'cm':38, -'ca':39, -'cv':40, -'ky':41, -'cf':42, -'td':43, -'cl':44, -'cn':45, -'cx':46, -'cc':47, -'co':48, -'km':49, -'cg':50, -'cd':51, -'ck':52, -'cr':53, -'ci':54, -'hr':55, -'cu':56, -'cw':57, -'cy':58, -'cz':59, -'dk':60, -'dj':61, -'dm':62, -'do':63, -'ec':64, -'eg':65, -'sv':66, -'gq':67, -'er':68, -'ee':69, -'et':70, -'fk':71, -'fo':72, -'fj':73, -'fi':74, -'fr':75, -'gf':76, -'pf':77, -'tf':78, -'ga':79, -'gm':80, -'ge':81, -'de':82, -'gh':83, -'gi':84, -'gr':85, -'gl':86, -'gd':87, -'gp':88, -'gu':89, -'gt':90, -'gg':91, -'gn':92, -'gw':93, -'gy':94, -'ht':95, -'hm':96, -'va':97, -'hn':98, -'hk':99, -'hu':100, -'is':101, -'in':102, -'id':103, -'ir':104, -'iq':105, -'ie':106, -'im':107, -'il':108, -'it':109, -'jm':110, -'jp':111, -'je':112, -'jo':113, -'kz':114, -'ke':115, -'ki':116, -'kp':117, -'kr':118, -'kw':119, -'kg':120, -'la':121, -'lv':122, -'lb':123, -'ls':124, -'lr':125, -'ly':126, -'li':127, -'lt':128, -'lu':129, -'mo':130, -'mk':131, -'mg':132, -'mw':133, -'my':134, -'mv':135, -'ml':136, -'mt':137, -'mh':138, -'mq':139, -'mr':140, -'mu':141, -'yt':142, -'mx':143, -'fm':144, -'md':145, -'mc':146, -'mn':147, -'me':148, -'ms':149, -'ma':150, -'mz':151, -'mm':152, -'na':153, -'nr':154, -'np':155, -'nl':156, -'nc':157, -'nz':158, -'ni':159, -'ne':160, -'ng':161, -'nu':162, -'nf':163, -'mp':164, -'no':165, -'om':166, -'pk':167, -'pw':168, -'ps':169, -'pa':170, -'pg':171, -'py':172, -'pe':173, -'ph':174, -'pn':175, -'pl':176, -'pt':177, -'pr':178, -'qa':179, -'re':180, -'ro':181, -'ru':182, -'rw':183, -'bl':184, -'sh':185, -'kn':186, -'lc':187, -'mf':188, -'pm':189, -'vc':190, -'ws':191, -'sm':192, -'st':193, -'sa':194, -'sn':195, -'rs':196, -'sc':197, -'sl':198, -'sg':199, -'sx':200, -'sk':201, -'si':202, -'sb':203, -'so':204, -'za':205, -'gs':206, -'ss':207, -'es':208, -'lk':209, -'sd':210, -'sr':211, -'sj':212, -'sz':213, -'se':214, -'ch':215, -'sy':216, -'tw':217, -'tj':218, -'tz':219, -'th':220, -'tl':221, -'tg':222, -'tk':223, -'to':224, -'tt':225, -'tn':226, -'tr':227, -'tm':228, -'tc':229, -'tv':230, -'ug':231, -'ua':232, -'ae':233, -'gb':234, -'us':235, -'um':236, -'uy':237, -'uz':238, -'vu':239, -'ve':240, -'vn':241, -'vg':242, -'vi':243, -'wf':244, -'eh':245, -'ye':246, -'zm':247, -'zw':248, -'unknown':500 -} - +country = {'af': 0, + 'ax': 1, + 'al': 2, + 'dz': 3, + 'as': 4, + 'ad': 5, + 'ao': 6, + 'ai': 7, + 'aq': 8, + 'ag': 9, + 'ar': 10, + 'am': 11, + 'aw': 12, + 'au': 13, + 'at': 14, + 'az': 15, + 'bs': 16, + 'bh': 17, + 'bd': 18, + 'bb': 19, + 'by': 20, + 'be': 21, + 'bz': 22, + 'bj': 23, + 'bm': 24, + 'bt': 25, + 'bo': 26, + 'bq': 27, + 'ba': 28, + 'bw': 29, + 'bv': 30, + 'br': 31, + 'io': 32, + 'bn': 33, + 'bg': 34, + 'bf': 35, + 'bi': 36, + 'kh': 37, + 'cm': 38, + 'ca': 39, + 'cv': 40, + 'ky': 41, + 'cf': 42, + 'td': 43, + 'cl': 44, + 'cn': 45, + 'cx': 46, + 'cc': 47, + 'co': 48, + 'km': 49, + 'cg': 50, + 'cd': 51, + 'ck': 52, + 'cr': 53, + 'ci': 54, + 'hr': 55, + 'cu': 56, + 'cw': 57, + 'cy': 58, + 'cz': 59, + 'dk': 60, + 'dj': 61, + 'dm': 62, + 'do': 63, + 'ec': 64, + 'eg': 65, + 'sv': 66, + 'gq': 67, + 'er': 68, + 'ee': 69, + 'et': 70, + 'fk': 71, + 'fo': 72, + 'fj': 73, + 'fi': 74, + 'fr': 75, + 'gf': 76, + 'pf': 77, + 'tf': 78, + 'ga': 79, + 'gm': 80, + 'ge': 81, + 'de': 82, + 'gh': 83, + 'gi': 84, + 'gr': 85, + 'gl': 86, + 'gd': 87, + 'gp': 88, + 'gu': 89, + 'gt': 90, + 'gg': 91, + 'gn': 92, + 'gw': 93, + 'gy': 94, + 'ht': 95, + 'hm': 96, + 'va': 97, + 'hn': 98, + 'hk': 99, + 'hu': 100, + 'is': 101, + 'in': 102, + 'id': 103, + 'ir': 104, + 'iq': 105, + 'ie': 106, + 'im': 107, + 'il': 108, + 'it': 109, + 'jm': 110, + 'jp': 111, + 'je': 112, + 'jo': 113, + 'kz': 114, + 'ke': 115, + 'ki': 116, + 'kp': 117, + 'kr': 118, + 'kw': 119, + 'kg': 120, + 'la': 121, + 'lv': 122, + 'lb': 123, + 'ls': 124, + 'lr': 125, + 'ly': 126, + 'li': 127, + 'lt': 128, + 'lu': 129, + 'mo': 130, + 'mk': 131, + 'mg': 132, + 'mw': 133, + 'my': 134, + 'mv': 135, + 'ml': 136, + 'mt': 137, + 'mh': 138, + 'mq': 139, + 'mr': 140, + 'mu': 141, + 'yt': 142, + 'mx': 143, + 'fm': 144, + 'md': 145, + 'mc': 146, + 'mn': 147, + 'me': 148, + 'ms': 149, + 'ma': 150, + 'mz': 151, + 'mm': 152, + 'na': 153, + 'nr': 154, + 'np': 155, + 'nl': 156, + 'nc': 157, + 'nz': 158, + 'ni': 159, + 'ne': 160, + 'ng': 161, + 'nu': 162, + 'nf': 163, + 'mp': 164, + 'no': 165, + 'om': 166, + 'pk': 167, + 'pw': 168, + 'ps': 169, + 'pa': 170, + 'pg': 171, + 'py': 172, + 'pe': 173, + 'ph': 174, + 'pn': 175, + 'pl': 176, + 'pt': 177, + 'pr': 178, + 'qa': 179, + 're': 180, + 'ro': 181, + 'ru': 182, + 'rw': 183, + 'bl': 184, + 'sh': 185, + 'kn': 186, + 'lc': 187, + 'mf': 188, + 'pm': 189, + 'vc': 190, + 'ws': 191, + 'sm': 192, + 'st': 193, + 'sa': 194, + 'sn': 195, + 'rs': 196, + 'sc': 197, + 'sl': 198, + 'sg': 199, + 'sx': 200, + 'sk': 201, + 'si': 202, + 'sb': 203, + 'so': 204, + 'za': 205, + 'gs': 206, + 'ss': 207, + 'es': 208, + 'lk': 209, + 'sd': 210, + 'sr': 211, + 'sj': 212, + 'sz': 213, + 'se': 214, + 'ch': 215, + 'sy': 216, + 'tw': 217, + 'tj': 218, + 'tz': 219, + 'th': 220, + 'tl': 221, + 'tg': 222, + 'tk': 223, + 'to': 224, + 'tt': 225, + 'tn': 226, + 'tr': 227, + 'tm': 228, + 'tc': 229, + 'tv': 230, + 'ug': 231, + 'ua': 232, + 'ae': 233, + 'gb': 234, + 'us': 235, + 'um': 236, + 'uy': 237, + 'uz': 238, + 'vu': 239, + 've': 240, + 'vn': 241, + 'vg': 242, + 'vi': 243, + 'wf': 244, + 'eh': 245, + 'ye': 246, + 'zm': 247, + 'zw': 248, + 'unknown': 500 + } + + def to_int(country_string): - '''Converts a 2 letter country string like fr to the appropriate enum value - If not found, returns the value for unknown''' + """ + Converts a 2-letter country string like fr to the appropriate enum value + If not found, returns the value for unknown + """ lowercase = country_string.lower() if lowercase in country.keys(): return country[lowercase] else: return country['unknown'] + def to_key(code): - '''Converts the enum value to a 2 letter country code. If not found, - returns unknown''' + """Converts the enum value to a 2 letter country code. If not found, + returns unknown""" for name, number in country.iteritems(): if code == number: return name return "unknown" - diff -Nru droidlysis-3.4.0/droidlysis droidlysis-3.4.1/droidlysis --- droidlysis-3.4.0/droidlysis 2021-08-19 09:28:51.000000000 +0000 +++ droidlysis-3.4.1/droidlysis 2022-01-27 12:02:01.000000000 +0000 @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- import droidlysis3 diff -Nru droidlysis-3.4.0/droidlysis3.py droidlysis-3.4.1/droidlysis3.py --- droidlysis-3.4.0/droidlysis3.py 2022-01-18 09:56:10.000000000 +0000 +++ droidlysis-3.4.1/droidlysis3.py 2023-02-21 13:41:32.000000000 +0000 @@ -7,7 +7,7 @@ import argparse import os import subprocess -import droidutil # that's my own utilities +import droidutil # that's my own utilities import droidsample import droidreport import sys @@ -15,23 +15,36 @@ property_dump_file = 'details.md' report_file = 'report.md' json_file = 'report.json' -__version__ = "3.4.0" +__version__ = "3.4.1" + def get_arguments(): """Read arguments for the program and returns the ArgumentParser""" parser = argparse.ArgumentParser(description='''DroidLysis3 is a Python -script which processes Android samples. \n -1/ It extracts properties from the samples (e.g connects to Internet, roots the phone...). The extracted properties are displayed.\n -2/ It helps the analyst begin its reverse engineering of the sample, by performing a first automatic analysis, disassembling, decompiling and a description draft.''', prog='DroidLysis', epilog='Version '+__version__+' - Greetz from Axelle Apvrille') - parser.add_argument('-i', '--input', help='input directories or files to process', nargs='+', action='store', default='.') - parser.add_argument('-o', '--output', help='analysis of input files is written into subdirectories of this directory', action='store', default='.') - parser.add_argument('-c', '--clearoutput', help='erase the output directory at the end. Indicates you want something quick.', action='store_true') + script which processes Android samples. \n + 1/ It extracts properties from the samples (e.g connects to Internet, roots the phone...). + The extracted properties are displayed.\n + 2/ It helps the analyst begin its reverse engineering of the sample, by performing a first automatic analysis, + disassembling, decompiling and a description draft.''', + prog='DroidLysis', epilog='Version '+__version__+' - Greetz from @cryptax') + parser.add_argument('-i', '--input', + help='input directories or files to process', nargs='+', action='store', default='.') + parser.add_argument('-o', '--output', + help='analysis of input files is written into subdirectories of this directory', + action='store', default='.') + parser.add_argument('-c', '--clearoutput', + help='erase the output directory at the end. Indicates you want something quick.', + action='store_true') parser.add_argument('-s', '--silent', help='do not display output on console', action='store_true') - parser.add_argument('-m', '--movein', help='after it has been processed, each input file is moved to this directory', action='store') + parser.add_argument('-m', '--movein', + help='after it has been processed, each input file is moved to this directory', action='store') parser.add_argument('-v', '--verbose', help='get more detailed messages', action='store_true') - parser.add_argument('-V', '--version', help='displays version number', action='version', version="%(prog)s "+__version__) - parser.add_argument('--no-kit-exception', help='by default, ad/dev/stats kits are ruled out for searches. Use this option to treat them as regular namespaces', action='store_true') + parser.add_argument('-V', '--version', help='displays version number', action='version', + version="%(prog)s "+__version__) + parser.add_argument('--no-kit-exception', + help='by default, ad/dev/stats kits are ruled out for searches. ' + 'Use this option to treat them as regular namespaces', action='store_true') parser.add_argument('--enable-procyon', help='enable procyon decompilation', action='store_true') parser.add_argument('--disable-report', help='do not generate automatic report', action='store_true') parser.add_argument('--enable-sql', help='write analysis to SQL database', action='store_true') @@ -49,6 +62,7 @@ return args + def process_input(args): """ Process input. @@ -63,33 +77,39 @@ if os.path.isdir(element): listing = os.listdir(element) for file in listing: - process_file(os.path.join(element, file), args.output, args.verbose, args.clearoutput, args.enable_procyon, args.disable_report, args.no_kit_exception, args.enable_sql, args.disable_json) + process_file(os.path.join(element, file), args.output, args.verbose, args.clearoutput, + args.enable_procyon, args.disable_report, args.no_kit_exception, + args.enable_sql, args.disable_json) if args.movein: if args.verbose: - print("Moving %s to %s" % (os.path.join('.',element), os.path.join(args.movein, element))) + print("Moving %s to %s" % (os.path.join('.', element), os.path.join(args.movein, element))) # TODO: issue if inner dirs. Are we handling this? try: os.rename(os.path.join(element, file), os.path.join(args.movein, file)) except OSError as e: if args.verbose: - print( "%s no longer present?: %s\n" % (file, str(e))) + print("%s no longer present?: %s\n" % (file, str(e))) if os.path.isfile(element): - process_file(os.path.join('.',element), args.output, args.verbose, args.clearoutput, args.enable_procyon, args.disable_report, args.silent, args.no_kit_exception) - # dirname = os.path.join(args.output, '{filename}-*'.format(filename=element)) + process_file(os.path.join('.', element), args.output, args.verbose, args.clearoutput, args.enable_procyon, + args.disable_report, args.silent, args.no_kit_exception) + # dir name = os.path.join(args.output, '{filename}-*'.format(filename=element)) if args.movein: if args.verbose: - print("Moving %s to %s" % (os.path.join('.',element), os.path.join(args.movein, os.path.basename(element)))) - os.rename(os.path.join('.',element), os.path.join(args.movein, os.path.basename(element))) + print("Moving %s to %s" % (os.path.join('.', element), os.path.join(args.movein, + os.path.basename(element)))) + os.rename(os.path.join('.', element), os.path.join(args.movein, os.path.basename(element))) -def process_file(infile, outdir='/tmp/analysis', verbose=False, clear=False, enable_procyon=False, disable_report=False, silent=False, no_kit_exception=False, enable_sql=False, disable_json=False): +def process_file(infile, outdir='/tmp/analysis', verbose=False, clear=False, enable_procyon=False, + disable_report=False, silent=False, no_kit_exception=False, enable_sql=False, disable_json=False): """Static analysis of a given file""" if os.access(infile, os.R_OK): if not silent: print("Processing: " + infile + " ...") - sample = droidsample.droidsample(infile, outdir, verbose, clear, enable_procyon, disable_report, silent, no_kit_exception) + sample = droidsample.droidsample(infile, outdir, verbose, clear, enable_procyon, + disable_report, silent, no_kit_exception) sample.unzip() sample.disassemble() sample.extract_file_properties() @@ -132,6 +152,7 @@ sample.close() + def check_python_version(): if sys.version_info.major < 3: print("ERROR: Please run DroidLysis with Python 3") diff -Nru droidlysis-3.4.0/droidlysis.egg-info/PKG-INFO droidlysis-3.4.1/droidlysis.egg-info/PKG-INFO --- droidlysis-3.4.0/droidlysis.egg-info/PKG-INFO 2022-01-18 12:44:52.000000000 +0000 +++ droidlysis-3.4.1/droidlysis.egg-info/PKG-INFO 2023-02-21 14:47:48.000000000 +0000 @@ -1,105 +1,91 @@ Metadata-Version: 2.1 Name: droidlysis -Version: 3.4.0 -Summary: DroidLysis: pre-analysis script for suspicious Android samples +Version: 3.4.1 +Summary: DroidLysis: pre-analysis of suspicious Android samples Home-page: https://github.com/cryptax/droidlysis Author: @cryptax Author-email: aafortinet@gmail.com License: MIT Keywords: android malware reverse -Platform: UNKNOWN Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License Classifier: Development Status :: 3 - Alpha Classifier: Operating System :: Unix Classifier: Topic :: Software Development :: Disassemblers -Requires-Python: >=3.0.* +Requires-Python: >=3.0 Description-Content-Type: text/markdown License-File: LICENSE # DroidLysis -DroidLysis is a **property extractor for Android apps**. -It automatically disassembles the Android application you provide -and looks for various properties within the package or its disassembly. +DroidLysis is a **pre-analysis tool for Android apps**: it performs repetitive and boring tasks we'd typically do at the beginning of any reverse engineering. It disassembles the Android sample, organizes output in directories, and searches for suspicious spots in the code to look at. +The output helps the reverse engineer speed up the first few steps of analysis. DroidLysis can be used over Android packages (apk), Dalvik executables (dex), Zip files (zip), Rar files (rar) or directories of files. - + ## Quick setup Can't wait to use DroidLysis? Then, use a Docker container: ``` -$ docker pull cryptax/droidlysis:2021.04 -$ docker run -it --rm -v /tmp/share:/share cryptax/droidlysis:2021.04 /bin/bash +$ docker pull cryptax/droidlysis:2023.02 +$ docker run -it --rm -v /tmp/share:/share cryptax/droidlysis:2023.02 /bin/bash +$ cd /opt/droidlysis +$ python3 ./droidlysis3.py --help ``` -DroidLysis is located in `/opt/droidlysis`. - ## Installing DroidLysis 1. Install required system packages 2. Install Android disassembly tools -3. Get DroidLysis from the Git repository or from pip +3. Get DroidLysis from the Git repository (preferred) or from pip 4. Configure `droidconfig.py` -### Step1: Install required system packages - -`sudo apt-get install default-jre git python3 python3-pip unzip wget libmagic-dev libxml2-dev libxslt-dev` - -### Step 2: Install Android disassembly tools - -DroidLysis does not perform the disassembly itself, but relies on other tools to do so. Therefore, you must install: - -- [Apktool](https://ibotpeaches.github.io/Apktool/) - note we only need the Jar. -- [Baksmali](https://bitbucket.org/JesusFreke/smali/downloads) - note we only need the Jar. +Install required system packages: -Optionally: - -- [Dex2jar](https://github.com/pxb1988/dex2jar) - dex2jar is now *optional*. If you don't need Dex to Jar transformation (useful for later decompiling!), you can skip it. -- [Procyon](https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler) - *optional*. If you don't want to use this decompiler, skip its installation. - -Some of these tools are redundant, but sometimes one fails on a sample while another does not. DroidLysis detects this and tries to switch to a tool that works for the sample. +``` +sudo apt-get install default-jre git python3 python3-pip unzip wget libmagic-dev libxml2-dev libxslt-dev +``` -For example, +Install Android disassembly tools: [Apktool](https://ibotpeaches.github.io/Apktool/) , +[Baksmali](https://bitbucket.org/JesusFreke/smali/downloads), and optionally +[Dex2jar](https://github.com/pxb1988/dex2jar) and +[Procyon](https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler) (note that Procyon only works with Java 8, not Java 11). ``` $ mkdir -p ~/softs $ cd ~/softs -$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.6.0.jar +$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.7.0.jar $ wget https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar -$ wget https://github.com/pxb1988/dex2jar/files/1867564/dex-tools-2.1-SNAPSHOT.zip -$ unzip dex-tools-2.1-SNAPSHOT.zip -$ rm -f dex-tools-2.1-SNAPSHOT.zip +$ wget https://github.com/pxb1988/dex2jar/releases/download/v2.2-SNAPSHOT-2021-10-31/dex-tools-2.2-SNAPSHOT-2021-10-31.zip +$ unzip dex-tools-2.2-SNAPSHOT-2021-10-31.zip +$ rm -f dex-tools-2.2-SNAPSHOT-2021-10-31.zip ``` -### Step 3a. Get DroidLysis from the Git Repository - -This is the most up-to-date version. - +Install from Git in a Python virtual environment: ``` -$ git clone https://github.com/cryptax/droidlysis -$ cd droidlysis -$ pip3 install -r requirements.txt +$ python3 -m venv venv +$ source ./venv/bin/activate +(venv) $ pip3 install git+https://github.com/cryptax/droidlysis ``` -### Step 3b. Get DroidLysis from Pypi - -Alternatively, you can install DroidLysis from pip3. Note the package may be slightly behind the git repository. +Run it: ``` -$ python3 -m venv droidlysis -$ cd droidlysis -$ source ./bin/activate -(droidlysis) /droidlysis # pip3 install droidlysis +cd droidlysis +./droidlysis --help ``` -### Step 4. Configure `droidconfig.py` +Alternatively, you can install DroidLysis directly from PyPi (`pip3 install droidlysis`). + +## Configuration -The configuration is extremely simple, you only need to tune `droidconfig.py`: +If you used the default install commands & directories as specified above, you won't need any configuration. + +The configuration is extremely simple, you only need to tune `droidconfig.py`. Note that if you placed the tools in the default `~/softs` directory as I specified, you don't have to do anything: the tools will be automatically found in that location. - `APKTOOL_JAR`: set the path to your apktool jar - `BAKSMALI_JAR`: set the path to your baksmali jar @@ -107,18 +93,19 @@ - `PROCYON_JAR`: set the path to the procyon decompiler jar. If you don't want Procyon, leave this path to a non existant file. - `INSTALL_DIR`: set the path to your DroidLysis instance. Do not forget to set this or DroidLysis won't work correctly! -Example: +By default, `droidconfig.py` searches for tools at the following location: ```python -APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.5.0.jar") +APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.7.0.jar") BAKSMALI_JAR = os.path.join(os.path.expanduser("~/softs"), "baksmali-2.5.2.jar") -DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.s -h") +DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.sh") PROCYON_JAR = os.path.join( os.path.expanduser("~/softs"), "procyon-decompiler-0.5.36.jar") INSTALL_DIR = os.path.expanduser("~/droidlysis") ``` -Optionally, if you need a specific situation, you might need to tune the following too. Normally, the default options will work and you won't have to touch these: +Optionally, if you need a specific situation, you might need to tune the following too. +Normally, the default options will work and you won't have to touch these: + - `SQLALCHEMY`: specify your SQL database. - `KEYTOOL`: absolute path of `keytool` which generally ships with Java - `SMALI_CONFIGFILE`: smali patterns @@ -132,34 +119,32 @@ DroidLysis uses **Python 3**. To launch it and get options: ``` -python3 ./droidlysis3.py --help +droidlysis --help ``` For example, test it on [Signal's APK](https://signal.org/android/apk/): ``` -python3 ./droidlysis3.py --input Signal-website-universal-release-4.52.4.apk --output /tmp +droidlysis --input Signal-website-universal-release-4.52.4.apk --output /tmp ``` -![](./example.png) +![](./images/example.png) DroidLysis outputs: -- A summary on the console (see example.png) +- A summary on the console (see image above) - The unzipped, pre-processed sample in a subdirectory of your output dir. The subdirectory is named using the sample's filename and sha256 sum. For example, if we analyze the Signal application and set `--output /tmp`, the analysis will be written to `/tmp/Signalwebsiteuniversalrelease4.52.4.apk-f3c7d5e38df23925dd0b2fe1f44bfa12bac935a6bc8fe3a485a4436d4487a290`. - A database (by default, SQLite `droidlysis.db`) containing properties it noticed. ## Options -Get usage with `python3 ./droidlysis3.py --help` +Get usage with `droidlysis --help` - The input can be a file or a directory of files to recursively look into. DroidLysis knows how to process Android packages, DEX, ODEX and ARM executables, ZIP, RAR. DroidLysis won't fail on other type of files (unless there is a bug...) but won't be able to understand the content. -- When processing directories of files, it is typically quite helpful to move processed samples to another location to know what has been processed. This is handled by option `--movein`. Also, if you are only interested in statistics, you should probably clear the output directory which contains detailed information for each sample: this is option `--clearoutput`. +- When processing directories of files, it is typically quite helpful to move processed samples to another location to know what has been processed. This is handled by option `--movein`. Also, if you are only interested in statistics, you should probably clear the output directory which contains detailed information for each sample: this is option `--clearoutput`. If you want to store all statistics in a SQL database, use `--enable-sql` (see [here](#sqlite_database)) -- When dealing with single samples, on the contrary, statistics are typically not so interesting, and their generation can be disabled with `--disable-sql` - -- DEX decompilation is quite long with Procyon, so this option is disabled by default. If you want to decompile to Java, use `--enable-procyon`. +- DEX decompilation is quite long with Procyon, so this option is *disabled* by default. If you want to decompile to Java, use `--enable-procyon`. - DroidLysis's analysis does not inspect known 3rd party SDK by default, i.e. for instance it won't report any suspicious activity from these. If you want them to be inspected, use option `--no-kit-exception`. This usually creates many more detected properties for the sample, as SDKs (e.g. advertisment) use lots of flagged APIs (get GPS location, get IMEI, get IMSI, HTTP POST...). @@ -182,7 +167,7 @@ If you do not need the sample output directory to be generated, use the option `--clearoutput`. -## SQLite database +## SQLite database{#sqlite_database} If you want to process a directory of samples, you'll probably like to store the properties DroidLysis found in a database, to easily parse and query the findings. In that case, use the option `--enable-sql`. This will automatically dump all results in a database named `droidlysis.db`, in a table named `samples`. Each entry in the table is relative to a given sample. Each column is properties DroidLysis tracks. @@ -210,22 +195,18 @@ description=Sending SMS messages ``` - ## To do -- The code is quite crappy now. I could probably do the same in less lines! -- Replace print by logging -- Remove the "caution: filename not matched: classes6.dex" which occurs at file extraction in droidsample.py +- Remove the "caution: filename not matched: classes6.dex" which occurs at file extraction in `droidsample.py` ## Updates -v3.4.0 - Multidex support -v3.3.1 - Improving detection of Base64 strings -v3.3.0 - Dumping data to JSON -v3.2.1 - IP address detection -v3.2.0 - Dex2jar is optional -v3.1.0 - Detection of Base64 strings - - +- v3.4.1 - Removed dependency to Androguard +- v3.4.0 - Multidex support +- v3.3.1 - Improving detection of Base64 strings +- v3.3.0 - Dumping data to JSON +- v3.2.1 - IP address detection +- v3.2.0 - Dex2jar is optional +- v3.1.0 - Detection of Base64 strings diff -Nru droidlysis-3.4.0/droidlysis.egg-info/requires.txt droidlysis-3.4.1/droidlysis.egg-info/requires.txt --- droidlysis-3.4.0/droidlysis.egg-info/requires.txt 2022-01-18 12:44:52.000000000 +0000 +++ droidlysis-3.4.1/droidlysis.egg-info/requires.txt 2023-02-21 14:47:48.000000000 +0000 @@ -1,4 +1,4 @@ -python-magic -SQLAlchemy -rarfile -androguard +configparser>=4.0.2 +python-magic==0.4.12 +SQLAlchemy>=1.1.1 +rarfile>=3.0 diff -Nru droidlysis-3.4.0/droidproperties.py droidlysis-3.4.1/droidproperties.py --- droidlysis-3.4.0/droidproperties.py 2022-01-18 08:51:20.000000000 +0000 +++ droidlysis-3.4.1/droidproperties.py 2023-02-21 13:41:32.000000000 +0000 @@ -10,40 +10,39 @@ import droidconfig import droidcountry import droidsql -import re -import os -import argparse import json from sqlalchemy.orm import sessionmaker import sqlalchemy.exc """This is where to set whether given fields have a meaning or not for a given file type""" -applicability = { 'file_size' : [ droidutil.APK, droidutil.DEX, droidutil.ARM, droidutil.CLASS, droidutil.ZIP, droidutil.RAR ], \ - 'file_small' : [ droidutil.APK, droidutil.DEX, droidutil.ARM, droidutil.CLASS, droidutil.ZIP, droidutil.RAR ],\ - 'file_nb_classes' : [ droidutil.APK, droidutil.DEX ],\ - 'file_nb_dir' : [ droidutil.APK, droidutil.DEX ],\ - 'file_innerzips' : [ droidutil.APK, droidutil.ZIP, droidutil.RAR ],\ - 'cert' : [droidutil.APK ],\ - 'manifest' : [droidutil.APK], \ - 'smali' : [droidutil.APK, droidutil.DEX], \ - 'wide' : [droidutil.APK, droidutil.DEX], \ - 'arm' : [droidutil.APK, droidutil.ARM], \ - 'dex' : [droidutil.APK, droidutil.DEX], \ - 'kit' : [droidutil.APK, droidutil.DEX], \ - } +applicability = {'file_size': [droidutil.APK, droidutil.DEX, droidutil.ARM, + droidutil.CLASS, droidutil.ZIP, droidutil.RAR], + 'file_small': [droidutil.APK, droidutil.DEX, droidutil.ARM, + droidutil.CLASS, droidutil.ZIP, droidutil.RAR], + 'file_nb_classes': [droidutil.APK, droidutil.DEX], + 'file_nb_dir': [droidutil.APK, droidutil.DEX], + 'file_innerzips': [droidutil.APK, droidutil.ZIP, droidutil.RAR], + 'cert': [droidutil.APK], + 'manifest': [droidutil.APK], + 'smali': [droidutil.APK, droidutil.DEX], + 'wide': [droidutil.APK, droidutil.DEX], + 'arm': [droidutil.APK, droidutil.ARM], + 'dex': [droidutil.APK, droidutil.DEX], + 'kit': [droidutil.APK, droidutil.DEX] + } class droidproperties: verbose = False """Extracted properties""" """Field allocation""" - certificate = { } - manifest = { } - smali = { } - wide = { } - arm = { } - dex = { } - kits = { } + certificate = {} + manifest = {} + smali = {} + wide = {} + arm = {} + dex = {} + kits = {} def __init__(self, samplename='', sha256='', verbose=False): """Properties concern a given sample identified by a basename (to be helpful) and a sha256 (real reference)""" @@ -52,7 +51,6 @@ self.sanitized_basename = samplename self.clear_fields() - def clear_fields(self): """Re-initialize all fields of the object - to default values""" self.file_nb_classes = 0 @@ -63,34 +61,36 @@ self.file_innerzips = False self.certificate.clear() - self.certificate = { 'av' : False, \ - 'algo' : None, \ - 'debug': False, \ - 'dev' : False, \ - 'famous' : False, \ - 'serialno' : None, \ - 'country': droidcountry.country['unknown'],\ - 'owner' : None,\ - 'timestamp' : None, \ - 'year' : 0,\ - 'unknown_country' : False } + self.certificate = {'av': False, + 'algo': None, + 'debug': False, + 'dev': False, + 'famous': False, + 'serialno': None, + 'country': droidcountry.country['unknown'], + 'owner': None, + 'timestamp': None, + 'year': 0, + 'unknown_country': False + } self.manifest.clear() self.manifest = { - 'activities' : [], \ - 'libraries' : [], \ - 'listens_incoming_sms' : False,\ - 'listens_outgoing_call' : False,\ - 'maxSDK' : 0,\ - 'main_activity': None, \ - 'minSDK' : 0,\ - 'package_name' : None, \ - 'permissions' : [], \ - 'providers' : [], \ - 'receivers' : [], \ - 'services' : [], \ - 'swf' : False,\ - 'targetSDK' : 0 } + 'activities': [], + 'libraries': [], + 'listens_incoming_sms': False, + 'listens_outgoing_call': False, + 'maxSDK': 0, + 'main_activity': None, + 'minSDK': 0, + 'package_name': None, + 'permissions': [], + 'providers': [], + 'receivers': [], + 'services': [], + 'swf': False, + 'targetSDK': 0 + } # automatically adding smali properties. self.smali.clear() @@ -98,8 +98,8 @@ for section in self.smaliconfig.get_sections(): self.smali[section] = False - self.smali['packed'] = False # This property is not in conf section as it is deduced from no main activity + loading DEX dynamically - self.smali['multidex']= [] + self.smali['packed'] = False # This property is not in conf section as it is deduced from no main activity + loading DEX dynamically + self.smali['multidex'] = [] # automatically adding wide properties self.wide.clear() @@ -119,13 +119,13 @@ self.arm[section] = False self.dex.clear() - self.dex = { 'magic' : 0, \ - 'odex' : False, \ - 'magic_unknown' : False,\ - 'bad_sha1' : False,\ - 'bad_adler32' : False,\ - 'big_header': False,\ - 'thuxnder' : False + self.dex = {'magic': 0, + 'odex': False, + 'magic_unknown': False, + 'bad_sha1': False, + 'bad_adler32': False, + 'big_header': False, + 'thuxnder': False } # automatically set to False kit properties @@ -139,19 +139,19 @@ def write(self): Session = sessionmaker(bind=droidsql.engine) session = Session() - sample = droidsql.Sample(sha256=self.sha256, \ - sanitized_basename=self.sanitized_basename, \ - file_nb_classes=self.file_nb_classes, \ - file_nb_dir=self.file_nb_dir,\ - file_size=self.file_size,\ - file_small=self.file_small,\ - filetype=self.filetype,\ - file_innerzips=self.file_innerzips,\ - manifest_properties=json.dumps(self.manifest), \ - smali_properties=json.dumps(self.smali),\ - wide_properties=json.dumps(self.wide),\ - arm_properties=json.dumps(self.arm),\ - dex_properties=json.dumps(self.dex),\ + sample = droidsql.Sample(sha256=self.sha256, + sanitized_basename=self.sanitized_basename, + file_nb_classes=self.file_nb_classes, + file_nb_dir=self.file_nb_dir, + file_size=self.file_size, + file_small=self.file_small, + filetype=self.filetype, + file_innerzips=self.file_innerzips, + manifest_properties=json.dumps(self.manifest), + smali_properties=json.dumps(self.smali), + wide_properties=json.dumps(self.wide), + arm_properties=json.dumps(self.arm), + dex_properties=json.dumps(self.dex), kits=json.dumps(self.kits)) session.add(sample) try: @@ -162,26 +162,23 @@ print("Sample is already in the database") def dump_json(self, filename='report.json'): - data = { 'sanitized_basename' : self.sanitized_basename, \ - 'file_nb_classes' : self.file_nb_classes, \ - 'file_nb_dir' : self.file_nb_dir, \ - 'file_size' : self.file_size, \ - 'file_small' : self.file_small, \ - 'filetype' : self.filetype, \ - 'file_innerzips' : self.file_innerzips,\ - 'manifest_properties' : self.manifest, \ - 'smali_properties' : self.smali,\ - 'wide_properties' : self.wide,\ - 'arm_properties' : self.arm,\ - 'dex_properties' : self.dex,\ - 'kits' : self.kits } + data = {'sanitized_basename': self.sanitized_basename, + 'file_nb_classes': self.file_nb_classes, + 'file_nb_dir': self.file_nb_dir, + 'file_size': self.file_size, + 'file_small': self.file_small, + 'filetype': self.filetype, + 'file_innerzips': self.file_innerzips, + 'manifest_properties': self.manifest, + 'smali_properties': self.smali, + 'wide_properties': self.wide, + 'arm_properties': self.arm, + 'dex_properties': self.dex, + 'kits': self.kits + } if self.verbose: print("-------------") print("Dumping to JSON file {}".format(filename)) f = open(filename, 'w') f.write(json.dumps(data)) f.close() - - - - diff -Nru droidlysis-3.4.0/droidreport.py droidlysis-3.4.1/droidreport.py --- droidlysis-3.4.0/droidreport.py 2022-01-18 10:06:23.000000000 +0000 +++ droidlysis-3.4.1/droidreport.py 2023-02-21 13:48:49.000000000 +0000 @@ -4,11 +4,7 @@ __author__ = "Axelle Apvrille" __license__ = "MIT License" """ -import os -import droidproperties -import droidutil -import xml.dom.minidom -import re + class droidversion: """A small class to hold different versions of Android""" @@ -18,38 +14,43 @@ self.apilevel = apilevel def __str__(self): - return "Android %s - %s (API level %d)" % (versionstring, codename, apilevel) - -versions = [ droidversion( '1.0', 1 ), - droidversion( '1.1', 2 ), - droidversion( '1.5', 3, 'Cupcake' ), - droidversion( '1.6', 4, 'Donut' ), - droidversion( '2.0', 5, 'Eclair' ), - droidversion( '2.0.1', 6, 'Eclair'), - droidversion( '2.1', 7, 'Eclair'), - droidversion( '2.2', 8, 'Froyo'), - droidversion( '2.3', 9, 'Gingerbread'), - droidversion( '2.3.3', 10, 'Gingerbread'), - droidversion( '3.0', 11, 'Honeycomb'), - droidversion( '3.1', 12, 'Honeycomb'), - droidversion( '3.2', 13, 'Honeycomb'), - droidversion( '4.0', 14, 'Ice Cream Sandwich'), - droidversion( '4.0.3', 15, 'Ice Cream Sandwich'), - droidversion( '4.1', 16, 'Jelly Bean'), - droidversion( '4.2', 17, 'Jelly Bean'), - droidversion( '4.3', 18, 'Jelly Bean'), - droidversion( '4.4', 19, 'KitKat'), - droidversion( '5.0', 21, 'Lollipop'), - droidversion( '5.1', 22, 'Lollipop'), - droidversion( '6.0', 23, 'Marshmallow'), - droidversion( '7.0', 24, 'Nougat'), - droidversion( '7.1', 25, 'Nougat'), - droidversion( '8.0', 26, 'Oreo'), - droidversion( '8.1', 27, 'Oreo'), - droidversion( '9.0', 28, 'Pie'), - droidversion( '10.0', 29, 'Q'), - droidversion( '11.0', 30, 'R'), - ] + return "Android %s - %s (API level %d)" % (self.versionstring, self.codename, self.apilevel) + + +versions = [droidversion('1.0', 1), + droidversion('1.1', 2), + droidversion('1.5', 3, 'Cupcake'), + droidversion('1.6', 4, 'Donut'), + droidversion('2.0', 5, 'Eclair'), + droidversion('2.0.1', 6, 'Eclair'), + droidversion('2.1', 7, 'Eclair'), + droidversion('2.2', 8, 'Froyo'), + droidversion('2.3', 9, 'Gingerbread'), + droidversion('2.3.3', 10, 'Gingerbread'), + droidversion('3.0', 11, 'Honeycomb'), + droidversion('3.1', 12, 'Honeycomb'), + droidversion('3.2', 13, 'Honeycomb'), + droidversion('4.0', 14, 'Ice Cream Sandwich'), + droidversion('4.0.3', 15, 'Ice Cream Sandwich'), + droidversion('4.1', 16, 'Jelly Bean'), + droidversion('4.2', 17, 'Jelly Bean'), + droidversion('4.3', 18, 'Jelly Bean'), + droidversion('4.4', 19, 'KitKat'), + droidversion('5.0', 21, 'Lollipop'), + droidversion('5.1', 22, 'Lollipop'), + droidversion('6.0', 23, 'Marshmallow'), + droidversion('7.0', 24, 'Nougat'), + droidversion('7.1', 25, 'Nougat'), + droidversion('8.0', 26, 'Oreo'), + droidversion('8.1', 27, 'Oreo'), + droidversion('9.0', 28, 'Pie'), + droidversion('10', 29, 'Q'), + droidversion('11', 30, 'R'), + droidversion('12.0', 31, 'Snow Cone'), + droidversion('12.1', 32, 'Snow Cone v2'), + droidversion('13', 33, 'Tiramisu'), + droidversion('14', 34, 'Upside Down Cake') + ] # ------------------------------------------------------ @@ -60,7 +61,6 @@ self.sample = sample self.console = console self.report_to_file = report_to_file - def write(self, report_file, verbose=True): if verbose: @@ -86,19 +86,22 @@ # Header / File info print("\033[1;36;1m============= Report ============\033[0m") self.write_file("{0:20.20}: {1}\n".format('Sanitized basename', self.sample.properties.sanitized_basename)) - self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('Sanitized basename', self.sample.properties.sanitized_basename)) + self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('Sanitized basename', + self.sample.properties.sanitized_basename)) self.write_file("{0:20.20}: {1}\n".format('SHA256', self.sample.properties.sha256)) self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('SHA256', self.sample.properties.sha256)) self.write_file("{0:20.20}: {1} bytes\n".format('File size', self.sample.properties.file_size)) - self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m bytes".format('File size', self.sample.properties.file_size)) + self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m bytes".format('File size', + self.sample.properties.file_size)) self.write_file("{0:20.20}: {1}\n".format('Is small', self.sample.properties.file_small)) self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('Is small', self.sample.properties.file_small)) self.write_file("{0:20.20}: {1}\n".format('Nb of classes', self.sample.properties.file_nb_classes)) - self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('Nb of classes', self.sample.properties.file_nb_classes)) + self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('Nb of classes', + self.sample.properties.file_nb_classes)) self.write_file("{0:20.20}: {1}\n".format('Nb of directories', self.sample.properties.file_nb_dir)) self.write_console("{0:20.20}: \033[1;37;1m{1}\033[0m".format('Nb of dirs', self.sample.properties.file_nb_dir)) @@ -110,18 +113,21 @@ for key in self.sample.properties.certificate.keys(): if self.sample.properties.certificate[key] is not False: - self.write_console("{0:20.20}: \033[1;36;1m{1}\033[0m".format(key, self.sample.properties.certificate[key] )) - self.write_file("{0:20.20}: {1}\n".format(key, self.sample.properties.certificate[key] )) + self.write_console("{0:20.20}: \033[1;36;1m{1}\033[0m".format(key, + self.sample.properties.certificate[key])) + self.write_file("{0:20.20}: {1}\n".format(key, self.sample.properties.certificate[key])) # Manifest properties self.write_file("\nManifest properties:\n") self.write_console("\n\033[0;30;47mManifest properties\033[0m") for key in self.sample.properties.manifest.keys(): - if self.sample.properties.manifest[key] is not False and self.sample.properties.manifest[key] is not None and self.sample.properties.manifest[key] : - self.write_file("{0:20.20}: {1}\n".format(key, self.sample.properties.manifest[key] )) - self.write_console("{0:20.20}: \033[1;36;1m{1}\033[0m".format(key, self.sample.properties.manifest[key] )) + if self.sample.properties.manifest[key] is not False \ + and self.sample.properties.manifest[key] is not None and self.sample.properties.manifest[key]: + self.write_file("{0:20.20}: {1}\n".format(key, self.sample.properties.manifest[key])) + self.write_console("{0:20.20}: \033[1;36;1m{1}\033[0m".format(key, + self.sample.properties.manifest[key])) else: - self.write_file("{0:20.20}: {1}\n".format(key, self.sample.properties.manifest[key] )) + self.write_file("{0:20.20}: {1}\n".format(key, self.sample.properties.manifest[key])) # Smali properties @@ -129,9 +135,15 @@ self.write_console("\n\033[0;30;47mSmali properties / What the Dalvik code does\033[0m") for section in self.sample.properties.smali.keys(): if self.sample.properties.smali[section] is not False: - if (type(self.sample.properties.smali[section]) is list and len(self.sample.properties.smali[section]) > 0) or (type(self.sample.properties.smali[section]) is bool): - self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, self.sample.properties.smali[section], self.sample.properties.smaliconfig.get_description(section))) - self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.smali[section], self.sample.properties.smaliconfig.get_description(section))) + if (type(self.sample.properties.smali[section]) is list and + len(self.sample.properties.smali[section]) > 0) or \ + (type(self.sample.properties.smali[section]) is bool): + self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, + self.sample.properties.smali[section], + self.sample.properties.smaliconfig.get_description(section))) + self.write_file("{0:20.20}: {1} ({2})\n".format(section, + self.sample.properties.smali[section], + self.sample.properties.smaliconfig.get_description(section))) else: self.write_file("{0:20.20}: {1}\n".format(section, self.sample.properties.smali[section])) @@ -139,12 +151,17 @@ self.write_file("\nWide properties\n") self.write_console("\n\033[0;30;47mWide properties / What Resources/Assets do\033[0m") for section in self.sample.properties.wide.keys(): - if self.sample.properties.wide[section] is not False and self.sample.properties.wide[section] is not None and self.sample.properties.wide[section]: + if self.sample.properties.wide[section] is not False and self.sample.properties.wide[section] is not None \ + and self.sample.properties.wide[section]: if self.sample.properties.wideconfig.get_description(section) is not None: - self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, self.sample.properties.wide[section], self.sample.properties.wideconfig.get_description(section))) - self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.wide[section], self.sample.properties.wideconfig.get_description(section))) + self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, + self.sample.properties.wide[section], + self.sample.properties.wideconfig.get_description(section))) + self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.wide[section], + self.sample.properties.wideconfig.get_description(section))) else: - self.write_console("{0:20.20}: \033[1;31;1m{1}\033[0m".format(section, self.sample.properties.wide[section])) + self.write_console("{0:20.20}: \033[1;31;1m{1}\033[0m".format(section, + self.sample.properties.wide[section])) self.write_file("{0:20.20}: {1}\n".format(section, self.sample.properties.wide[section])) else: # case where the property is False, or None. @@ -156,8 +173,11 @@ for section in self.sample.properties.arm.keys(): if self.sample.properties.arm[section] is not False and self.sample.properties.arm[section] is not None: if self.sample.properties.armconfig.get_description(section) is not None: - self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, self.sample.properties.arm[section], self.sample.properties.armconfig.get_description(section))) - self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.arm[section], self.sample.properties.armconfig.get_description(section))) + self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, + self.sample.properties.arm[section], + self.sample.properties.armconfig.get_description(section))) + self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.arm[section], + self.sample.properties.armconfig.get_description(section))) else: self.write_console("{0:20.20}: \033[1;31;1m{1}\033[0m".format(section, self.sample.properties.arm[section])) self.write_file("{0:20.20}: {1}\n".format(section, self.sample.properties.arm[section])) @@ -170,7 +190,8 @@ self.write_console("\n\033[0;30;47mDEX properties / About classes.dex format\033[0m") for section in self.sample.properties.dex.keys(): if self.sample.properties.dex[section] is not False and self.sample.properties.dex[section] is not None: - self.write_console("{0:20.20}: \033[1;31;1m{1} \033[0m".format(section, self.sample.properties.dex[section])) + self.write_console("{0:20.20}: \033[1;31;1m{1} \033[0m".format(section, + self.sample.properties.dex[section])) self.write_file("{0:20.20}: {1}\n".format(section, self.sample.properties.dex[section])) else: # case where the property is False, or None. @@ -182,10 +203,14 @@ for section in self.sample.properties.kits.keys(): if self.sample.properties.kits[section] is not False and self.sample.properties.kits[section] is not None: if self.sample.properties.kitsconfig.get_description(section) is not None: - self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, self.sample.properties.kits[section], self.sample.properties.kitsconfig.get_description(section))) - self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.kits[section], self.sample.properties.kitsconfig.get_description(section))) + self.write_console("{0:20.20}: \033[1;31;1m{1} \033[1;33;40m({2})\033[0m".format(section, + self.sample.properties.kits[section], + self.sample.properties.kitsconfig.get_description(section))) + self.write_file("{0:20.20}: {1} ({2})\n".format(section, self.sample.properties.kits[section], + self.sample.properties.kitsconfig.get_description(section))) else: - self.write_console("{0:20.20}: \033[1;31;1m{1}\033[0m".format(section, self.sample.properties.kits[section])) + self.write_console("{0:20.20}: \033[1;31;1m{1}\033[0m".format(section, + self.sample.properties.kits[section])) self.write_file("{0:20.20}: {1}\n".format(section, self.sample.properties.kits[section])) else: # case where the property is False, or None. diff -Nru droidlysis-3.4.0/droidsample.py droidlysis-3.4.1/droidsample.py --- droidlysis-3.4.0/droidsample.py 2022-01-18 09:55:56.000000000 +0000 +++ droidlysis-3.4.1/droidsample.py 2023-02-21 14:44:42.000000000 +0000 @@ -3,13 +3,11 @@ """ __author__ = "Axelle Apvrille" __license__ = "MIT License" -__version__ = '3.4.0' """ import hashlib import base64 import os import re -import itertools import sys import string import struct @@ -25,32 +23,38 @@ import droidziprar import droidurl import xml.parsers.expat as expat +import logging + +logging.basicConfig(format='%(levelname)s:%(filename)s:%(message)s', level=logging.INFO) + class droidsample: - """Base class for an Android sample to analyze""" + # Base class for an Android sample to analyze - def __init__(self, filename, output='/tmp/analysis', verbose=False, clear=False, enable_procyon=False, disable_description=False, silent=False, no_kit_exception=False): - """Setup analysis of a given sample. This does not perform the analysis in itself.""" + def __init__(self, filename, output='/tmp/analysis', verbose=False, clear=False, enable_procyon=False, + disable_description=False, silent=False, no_kit_exception=False): + # Setup analysis of a given sample. This does not perform the analysis in itself - assert filename != None, "Filename is invalid" + assert filename is not None, "Filename is invalid" self.absolute_filename = filename - self.verbose = verbose self.clear = clear self.enable_procyon = enable_procyon self.disable_description = disable_description # we need those for recursive calls to process_file self.silent = silent self.no_kit_exception = no_kit_exception - self.ziprar = None # zip file or rar file file handle + self.ziprar = None # zip file or rar file file handle + self.verbose = verbose + if verbose: + logging.getLogger().setLevel(logging.DEBUG) sanitized_basename = droidutil.sanitize_filename(os.path.basename(filename)) if not silent: - print("Filename: "+filename) - self.properties = droidproperties.droidproperties(samplename=sanitized_basename,\ - sha256=droidutil.sha256sum(filename), \ - verbose=self.verbose) - if verbose: - print( "SHA256: %s" % (self.properties.sha256)) + logging.info("Filename: "+filename) + self.properties = droidproperties.droidproperties(samplename=sanitized_basename, + sha256=droidutil.sha256sum(filename), + verbose=verbose) + logging.debug("SHA256: %s" % (self.properties.sha256)) """Computing the SHA1 of a file is only useful to help out the analyst. The digest is written to the automatic analysis/description of the sample. That @@ -59,25 +63,21 @@ does not want to know about the SHA1 digest, and thus it's useless to compute it.""" if not clear: sha1 = droidutil.sha1sum(filename) - if verbose: - print( "SHA1: %s" % (sha1)) - + logging.debug("SHA1: %s" % (sha1)) # Creating the output analysis directory - self.outdir = os.path.join(output, '{filename}-{hash}'.format(\ - filename=sanitized_basename,\ + self.outdir = os.path.join(output, '{filename}-{hash}'.format( + filename=sanitized_basename, hash=self.properties.sha256)) - if verbose: - print( "Output analysis directory: " + self.outdir ) + logging.debug("Output analysis directory: " + self.outdir) if os.path.exists(self.outdir): try: shutil.rmtree(self.outdir, ignore_errors=droidutil.on_rm_tree_error) os.makedirs(self.outdir) except RuntimeError: - if verbose: - print( "Failed to remove directory - rmtree / RuntimeError" ) + logging.debug("Failed to remove directory - rmtree / RuntimeError") else: os.makedirs(self.outdir) @@ -88,9 +88,8 @@ self.process_output = open("/dev/null", 'w') def close(self): - if self.process_output != None: + if self.process_output is not None: self.process_output.close() - def unzip(self): """ @@ -101,60 +100,57 @@ Returns the file type of the sample: droidutil. (UNKNOWN, APK, DEX, ...) """ - if self.verbose: - print("------------- Unzipping %s" % (self.absolute_filename)) + logging.debug("------------- Unzipping %s" % (self.absolute_filename)) self.properties.filetype = droidutil.get_filetype(self.absolute_filename) if self.properties.filetype == droidutil.ARM or \ self.properties.filetype == droidutil.UNKNOWN or \ self.properties.filetype == droidutil.DEX: - if self.verbose: - print( "This is a %s. Nothing to unzip for %s" % (droidutil.str_filetype(self.properties.filetype), self.absolute_filename) ) + logging.debug("This is a %s. Nothing to unzip for %s" % (droidutil.str_filetype(self.properties.filetype), + self.absolute_filename)) return self.properties.filetype if self.properties.filetype == droidutil.ZIP or \ self.properties.filetype == droidutil.RAR: if self.properties.filetype == droidutil.ZIP: - self.ziprar = droidziprar.droidziprar(self.absolute_filename, \ - zipmode=True, verbose=self.verbose) + self.ziprar = droidziprar.droidziprar(self.absolute_filename, + zipmode=True, verbose=self.verbose) else: - self.ziprar = droidziprar.droidziprar(self.absolute_filename, \ - zipmode=False, verbose=self.verbose) - if self.ziprar.handle == None: - self.properties.filetype = droidutil.UNKNOWN # damaged zip/rar - if self.verbose: - print( "We are unable to unzip/unrar %s because of errors" % (self.absolute_filename) ) + self.ziprar = droidziprar.droidziprar(self.absolute_filename, + zipmode=False, verbose=self.verbose) + if self.ziprar.handle is None: + self.properties.filetype = droidutil.UNKNOWN # damaged zip/rar + logging.debug("We are unable to unzip/unrar %s because of errors" % (self.absolute_filename)) return droidutil.UNKNOWN # Now, we know self.ziprar is valid and open. self.properties.filetype, innerzips = self.ziprar.get_type() if innerzips: self.properties.file_innerzips = True - if self.verbose: - print( "There are inner zips/rars in " + self.absolute_filename ) + logging.debug("There are inner zips/rars in " + self.absolute_filename) for element in innerzips: # extract the inner zip/rar - if self.verbose: - print( "Extracting " + element + " inside " + self.absolute_filename ) + logging.debug("Extracting " + element + " inside " + self.absolute_filename) try: self.ziprar.extract_one_file(element, self.outdir) - if self.verbose: - print( "Recursively processing " + os.path.join(self.outdir, element) ) - droidlysis3.process_file(os.path.join(self.outdir, element), self.outdir, self.verbose, self.clear, self.enable_procyon, self.disable_description, self.no_kit_exception) + logging.debug("Recursively processing " + os.path.join(self.outdir, element)) + # TODO: this is probably buggy: we should be providing enable_sql too etc. + droidlysis3.process_file(os.path.join(self.outdir, element), + self.outdir, self.verbose, self.clear, + self.enable_procyon, self.disable_description, self.no_kit_exception) except: - print( "Cannot extract %s : %s" % (element, sys.exc_info()[0]) ) + logging.warning("Cannot extract %s : %s" % (element, sys.exc_info()[0])) if self.properties.filetype == droidutil.APK: # our zip actually is an APK if not self.clear: # let's unzip - if self.verbose: - print( "Unzipping " + self.absolute_filename + " to " + os.path.join(self.outdir, 'unzipped')) + logging.debug("Unzipping " + self.absolute_filename + " to " + os.path.join(self.outdir, 'unzipped')) try: self.ziprar.extract_all(outdir=os.path.join(self.outdir, 'unzipped')) except: - print( "Unzipping failed (catching exception): %s" % (sys.exc_info()[0])) + logging.warning("Unzipping failed (catching exception): %s" % (sys.exc_info()[0])) return self.properties.filetype @@ -167,7 +163,6 @@ java -jar apktool.jar [-q] d file.apk outdir/apktool if apktool failed java -jar baksmali.jar -o outdir/smali classes.dex in file.apk - androaxml.py --input binary-manifest in file.apk --output outdir/AndroidManifest.text.xml if clear unset, unzip file.apk -d outdir/unzipped else @@ -197,27 +192,26 @@ self.properties.filetype == droidutil.CLASS or \ self.properties.filetype == droidutil.UNKNOWN: # TODO: we could be running procyon on a class file. - if self.verbose: - print("Nothing to disassemble for " + self.absolute_filename) + logging.debug("Nothing to disassemble for " + self.absolute_filename) return if self.properties.filetype == droidutil.APK: # APKTOOL won't output to an existing dir unless you use the -f switch. But then, -f erases the contents # of the output dir... So we can't do this directly on self.outdir apktool_outdir = os.path.join(self.outdir, "apktool") - if self.verbose: - print("Running apktool on inputfile=" + self.absolute_filename) + logging.debug("Running apktool on inputfile=" + self.absolute_filename) if self.verbose: - print("Apktool command: java -jar %s d -f %s %s" % (droidconfig.APKTOOL_JAR, self.absolute_filename, apktool_outdir)) - subprocess.call(["java", "-jar", droidconfig.APKTOOL_JAR, \ - "d", "-f", self.absolute_filename, \ - "-o", apktool_outdir ]) + logging.debug("Apktool command: java -jar %s d -f %s %s" % (droidconfig.APKTOOL_JAR, + self.absolute_filename, apktool_outdir)) + subprocess.call(["java", "-jar", droidconfig.APKTOOL_JAR, + "d", "-f", self.absolute_filename, + "-o", apktool_outdir]) else: # with quiet option - subprocess.call(["java", "-jar", droidconfig.APKTOOL_JAR, \ - "-q", "d", "-f", self.absolute_filename, \ - "-o", apktool_outdir ], stdout=self.process_output, stderr=self.process_output) + subprocess.call(["java", "-jar", droidconfig.APKTOOL_JAR, + "-q", "d", "-f", self.absolute_filename, + "-o", apktool_outdir], stdout=self.process_output, stderr=self.process_output) if os.path.isdir(apktool_outdir): droidutil.move_dir(apktool_outdir, self.outdir) @@ -225,36 +219,33 @@ # we want all smali_classes? dir in smali if os.path.exists(os.path.join(self.outdir, "smali_classes2")): # we have multidex - we are going to move all smali classes in the same directory - if self.verbose: - print("Moving multidex smali classes to ./smali") + logging.debug("Moving multidex smali classes to ./smali") - os.system("cp -R "+ os.path.join(self.outdir, "./smali_classes?/*") + " " + os.path.join(self.outdir, "./smali") ) - os.system("rm -r "+ os.path.join(self.outdir, "./smali_classes?") ) + os.system("cp -R " + os.path.join(self.outdir, "./smali_classes?/*") + + " " + os.path.join(self.outdir, "./smali")) + os.system("rm -r " + os.path.join(self.outdir, "./smali_classes?")) if self.verbose: - print( "Apktool finished" ) + logging.debug("Apktool finished") # extract classes.dex whatever happens, we'll use it - if self.verbose: - print( "Extracting classes*.dex" ) + logging.debug("Extracting classes*.dex") try: self.ziprar.extract_one_file('classes.dex', self.outdir) - for i in range(2,10): + for i in range(2, 10): self.ziprar.extract_one_file('classes{}.dex'.format(i), self.outdir) if not os.path.exists(os.path.join(self.outdir, 'classes{}.dex'.format(i))): break self.properties.smali['multidex'].append('classes{}.dex'.format(i)) except: - if self.verbose: - print( "Extracting classes.dex failed: %s" % (sys.exc_info()[0])) - else: - print( "Extracting classes.dex failed") + logging.debug("Extracting classes.dex failed: %s" % (sys.exc_info()[0])) + logging.warning("Extracting classes.dex failed") # Disassemble the DEX(es) dex_files = [] if self.properties.filetype == droidutil.DEX: - dex_files.append( self.absolute_filename ) + dex_files.append(self.absolute_filename) else: if len(self.properties.smali['multidex']) > 0: for d in self.properties.smali['multidex']: @@ -262,88 +253,75 @@ else: dex_files.append(os.path.join(self.outdir, 'classes.dex')) - smali_dir = os.path.join( self.outdir, 'smali' ) + smali_dir = os.path.join(self.outdir, 'smali') - if (not os.access( smali_dir, os.R_OK) or \ + if (not os.access(smali_dir, os.R_OK) or (os.access(smali_dir, os.R_OK) and not os.listdir(smali_dir))): # we only do this if apktool didn't baksmali correctly for d in dex_files: if os.access(d, os.R_OK): - if self.verbose: - print( "Baksmali on {} -> {}".format(d, smali_dir)) + logging.debug("Baksmali on {} -> {}".format(d, smali_dir)) try: - subprocess.call( [ "java", "-jar", droidconfig.BAKSMALI_JAR, \ - "d", "-o", smali_dir, d ], \ - stdout=self.process_output, stderr=self.process_output) + subprocess.call(["java", "-jar", droidconfig.BAKSMALI_JAR, + "d", "-o", smali_dir, d], + stdout=self.process_output, stderr=self.process_output) except: - print( "[-] Baksmali failed for {}".format(d) ) + logging.warning("[-] Baksmali failed for {}".format(d)) # Decompile the DEX(es) - if self.verbose: - print("------------- Decompiling") + logging.debug("------------- Decompiling") if not self.clear: for d in dex_files: if os.access(d, os.R_OK): - jar_file = os.path.join(self.outdir, '{}-dex2jar.jar'.format(os.path.splitext(os.path.basename(d))[0])) - if os.access( droidconfig.DEX2JAR_CMD, os.X_OK): + jar_file = os.path.join(self.outdir, '{}-dex2jar.jar'.format( + os.path.splitext(os.path.basename(d))[0])) + if os.access(droidconfig.DEX2JAR_CMD, os.X_OK): if self.verbose: - print( "Dex2jar on " + d ) + logging.debug("Dex2jar on " + d) try: - subprocess.call( [ droidconfig.DEX2JAR_CMD, "--force", d, "-o", jar_file ], \ - stdout=self.process_output, stderr=self.process_output) + subprocess.call([droidconfig.DEX2JAR_CMD, "--force", d, "-o", jar_file], + stdout=self.process_output, stderr=self.process_output) except: - if self.verbose: - print("[-] Dex2jar failed on {}".format(d)) + logging.warning("[-] Dex2jar failed on {}".format(d)) else: - if self.verbose: - print("Dex2jar software is not executable, skipping (file: {0})".format(droidconfig.DEX2JAR_CMD)) + logging.warning("Dex2jar software is not executable, skipping (file: {0})".format(droidconfig.DEX2JAR_CMD)) - if os.access( jar_file, os.R_OK ): - if self.enable_procyon and os.access( droidconfig.PROCYON_JAR, os.R_OK ): - if self.verbose: - print( "Procyon decompiler on " + jar_file ) - subprocess.call( [ "java", "-jar", droidconfig.PROCYON_JAR, \ - jar_file, "-o", os.path.join(self.outdir, 'procyon') ], \ - stdout=self.process_output, stderr=self.process_output) + if os.access(jar_file, os.R_OK): + if self.enable_procyon and os.access(droidconfig.PROCYON_JAR, os.R_OK): + logging.debug("Procyon decompiler on " + jar_file) + subprocess.call(["java", "-jar", droidconfig.PROCYON_JAR, + jar_file, "-o", os.path.join(self.outdir, 'procyon')], + stdout=self.process_output, stderr=self.process_output) - if self.verbose: - print( "Unjarring " + jar_file ) + logging.debug("Unjarring " + jar_file) jarziprar = droidziprar.droidziprar(jar_file, zipmode=True, verbose=self.verbose) - if jarziprar.handle == None: - if self.verbose: - print( "Bad Jar / Failed to unjar " + jar_file ) + if jarziprar.handle is None: + logging.debug("Bad Jar / Failed to unjar " + jar_file) else: try: jarziprar.extract_all(os.path.join(self.outdir, 'unjarred')) except: - print( "Failed to unjar: %s" % (sys.exc_info()[0]) ) + logging.warning("Failed to unjar: %s" % (sys.exc_info()[0])) jarziprar.close() # Convert binary Manifest if self.properties.filetype == droidutil.APK: - manifest = os.path.join( self.outdir, 'AndroidManifest.xml') - if not os.access( manifest, os.R_OK) or os.path.getsize(manifest)==0: - if self.verbose: - print( "Extracting binary AndroidManifest.xml") - try: - self.ziprar.extract_one_file('AndroidManifest.xml', self.outdir) - except: - print("Failed to extract binary manifest: %s" % (sys.exc_info()[0])) - if os.access( manifest, os.R_OK) and os.path.getsize(manifest)>0: - textmanifest = os.path.join( self.outdir, 'AndroidManifest.text.xml') - subprocess.call( [ "androaxml.py", "--input", manifest, \ - "--output", textmanifest ], \ - stdout=self.process_output, stderr=self.process_output) - if os.access( textmanifest, os.R_OK ): - # overwrite the binary manifest with the converted text one - os.rename(textmanifest, manifest) + manifest = os.path.join(self.outdir, 'AndroidManifest.xml') + if not os.access(manifest, os.R_OK) or os.path.getsize(manifest) == 0: + logging.warning("Failed to extract binary manifest") + """ + we could attempt to do it with androaxml.py, but that introduces a dependancy to androguard + just for that we could do it with ~/Android/Sdk/tools/bin/apkanalyzer manifest print package.apk, + but that means finding apkanalyzer on the user's host + being sure they have Java 8 as the tool + does not work with Java 11. + it's simply not worth doing it, as this case happens very very seldom. + """ def extract_file_properties(self): """Extracts file size, nb of dirs and classes in smali dir""" - if self.verbose: - print("------------- Extracting file properties") + logging.debug("------------- Extracting file properties") self.properties.file_size = os.stat(self.absolute_filename).st_size @@ -355,11 +333,10 @@ if os.access(smali_dir, os.R_OK): self.properties.file_nb_dir, self.properties.file_nb_classes = droidutil.count_filedirs(smali_dir) - if self.verbose: - print( "Filesize: %d" % (self.properties.file_size)) - print( "Is Small: %d" % (self.properties.file_small)) - print( "Nb Class: %d" % (self.properties.file_nb_classes)) - print( "Nb Dir : %d" % (self.properties.file_nb_dir)) + logging.debug("Filesize: %d" % (self.properties.file_size)) + logging.debug("Is Small: %d" % (self.properties.file_small)) + logging.debug("Nb Class: %d" % (self.properties.file_nb_classes)) + logging.debug("Nb Dir : %d" % (self.properties.file_nb_dir)) def extract_meta_properties(self): """Extracting meta-data related to APK's signature timestamp and signing certificate""" @@ -367,12 +344,10 @@ self.properties.certificate['timestamp'] = self.ziprar.get_date('META-INF/MANIFEST.MF') if self.properties.certificate['timestamp'] != None: self.properties.certificate['year'] = self.properties.certificate['timestamp'][0] - if self.verbose: - print( "APK timestamp: %d" % (self.properties.certificate['timestamp'][0])) + logging.debug("APK timestamp: %d" % (self.properties.certificate['timestamp'][0])) # extract properties from the certificate - if self.verbose: - print( "------------- Extracting properties from certificate") + logging.debug("------------- Extracting properties from certificate") list = [] try: @@ -380,14 +355,13 @@ if not list: list = self.ziprar.extract_pattern(self.outdir, 'META-INF/.*\.DSA') except: - if self.verbose: - print( "Error at extraction: %s" % (sys.exc_info()[0])) + logging.debug("Error at extraction: %s" % (sys.exc_info()[0])) if list: try: - keyout = subprocess.check_output( [ droidconfig.KEYTOOL, "-printcert", "-file", \ - os.path.join(self.outdir, list[0]) ], \ - stderr=self.process_output).decode('utf-8') + keyout = subprocess.check_output([droidconfig.KEYTOOL, "-printcert", "-file", + os.path.join(self.outdir, list[0])], + stderr=self.process_output).decode('utf-8') keyout = re.sub(': ', '#', keyout) keyout = re.sub('\t| ', '', keyout) keyout = re.sub('until#', '\nUntil#', keyout) @@ -395,107 +369,97 @@ try: index = keysplit.index("Owner") self.properties.certificate['owner'] = keysplit[index+1] - if self.verbose: - print( "Certificate Owner: "+self.properties.certificate['owner']) + logging.debug("Certificate Owner: "+self.properties.certificate['owner']) self.extract_certificate_owner_properties(self.properties.certificate['owner']) except ValueError: - if self.verbose: - print( "Certificate Owner not present") + logging.debug("Certificate Owner not present") try: index = keysplit.index("Serialnumber") self.properties.certificate['serialno'] = keysplit[index+1] - if self.verbose: - print( "Certificate Serial no: "+self.properties.certificate['serialno']) + logging.debug("Certificate Serial no: "+self.properties.certificate['serialno']) # detect typical dev certificate if re.search('936eacbe07f201df', self.properties.certificate['serialno'], re.IGNORECASE): self.properties.certificate['dev'] = True - if self.verbose: - print( "Dev certificate detected") + logging.debug("Dev certificate detected") except ValueError: - if self.verbose: - print( "Serial number not present") + logging.debug("Serial number not present") try: index = keysplit.index("Signaturealgorithmname") self.properties.certificate['algo'] = keysplit[index+1] - if self.verbose: - print( "Algo: "+self.properties.certificate['algo'] ) + logging.debug("Algo: "+self.properties.certificate['algo']) except ValueError: - if self.verbose: - print( "Signature algorithm name not present") + logging.debug("Signature algorithm name not present") except subprocess.CalledProcessError as e: - if self.verbose: - print( "Caught CalledProcessError: ", e.output) - print( "Probably an invalid certificate" ) + logging.debug("Caught CalledProcessError: ", e.output) + logging.debug("Probably an invalid certificate") else: - if self.verbose: - print( "No certificate found" ) + logging.debug("No certificate found") def extract_certificate_owner_properties(self, owner): # owner should not be null m = re.search('C=(\w*)', owner, re.IGNORECASE) - if m != None: + if m is not None: cert_country = m.group(0)[2:] if re.search('Unknown', cert_country, re.IGNORECASE): self.properties.certificate['unknown_country'] = True else: self.properties.certificate['country'] = droidcountry.to_int(cert_country) - if self.verbose: - print( "Certificate Country = %s (%d)" % (cert_country, self.properties.certificate['country']) ) + logging.debug("Certificate Country = %s (%d)" % (cert_country, self.properties.certificate['country'])) # Debug certificate m = re.search('OU=Android O=Android L=Mountain View', owner, re.IGNORECASE) - if m != None: + if m is not None: self.properties.certificate['debug'] = True # AV owner - av_list = ('O=www.netqin.com',\ - 'OU=Symantec',\ - 'CN=Fortinet Android OU=Android O=Fortinet L=Burnaby ST=British Columbia C=BC',\ - 'OU=Engineering O=Webroot Software Inc L=Boulder ST=Colorado C=US|CN=James Burgess',\ - 'OU=Mobile Security, O=Flexilis, L=Los Angeles, ST=CA, C=US|O=Sophos Ltd. L=Abingdon C=UK',\ - 'OU=Application Development O=G Data Software AG L=Bochum ST=NRW C=DE',\ - 'CN=VIRUSTOTAL OU=VIRUSTOTAL O=VIRUSTOTAL L=Malaga ST=Malaga C=ES',\ - 'CN=Qihoo OU=Qihoo 360 Technology Co Ltd O=Qihoo 360 Technology Co Ltd L=Chaoyang ST=Beijing C=CN',\ - 'CN=KAIDI,OU=KAIDI,O=KAIDI,L=Beijing,ST=Beijing,C=86',\ + av_list = ('O=www.netqin.com', + 'OU=Symantec', + 'CN=Fortinet Android OU=Android O=Fortinet L=Burnaby ST=British Columbia C=BC', + 'OU=Engineering O=Webroot Software Inc L=Boulder ST=Colorado C=US|CN=James Burgess', + 'OU=Mobile Security, O=Flexilis, L=Los Angeles, ST=CA, C=US|O=Sophos Ltd. L=Abingdon C=UK', + 'OU=Application Development O=G Data Software AG L=Bochum ST=NRW C=DE', + 'CN=VIRUSTOTAL OU=VIRUSTOTAL O=VIRUSTOTAL L=Malaga ST=Malaga C=ES', + 'CN=Qihoo OU=Qihoo 360 Technology Co Ltd O=Qihoo 360 Technology Co Ltd L=Chaoyang ST=Beijing C=CN', + 'CN=KAIDI,OU=KAIDI,O=KAIDI,L=Beijing,ST=Beijing,C=86', 'EMAILADDRESS=android\@avast.com CN=avast! Android O=AVAST Software a.s. L=Prague ST=Prague C=CZ') for av in av_list: m = re.search(av, owner, re.IGNORECASE) - if m != None: + if m is not None: if self.verbose: - print( "Certificate owner matches AV : "+m.group(0) ) - self.certificate['av'] = True + print("Certificate owner matches AV : "+m.group(0)) + self.properties.certificate['av'] = True # Companies - famous_list = ('CN=Android OU=Android O=Google Inc\. L=Moutain View ST=California C=US',\ - 'O=Rovio Mobile Ltd L=Helsinki C=FI',\ + famous_list = ('CN=Android OU=Android O=Google Inc\. L=Moutain View ST=California C=US', + 'O=Rovio Mobile Ltd L=Helsinki C=FI', 'CN=Facebook Corporation OU=Facebook O=Facebook Mobile L=Palo Alto ST=CA C=US') for f in famous_list: m = re.search(f, owner, re.IGNORECASE) - if m != None: - if self.verbose: - print( "Certificate owner matches Famous : "+m.group(0) ) - self.certificate['famous'] = True + if m is not None: + logging.debug("Certificate owner matches Famous : "+m.group(0)) + self.properties.certificate['famous'] = True def is_packed(self): """ - We assume a sample is packed if the main activity its manifest references cannot be found in the DEX + DexClassLoader or Dex File + We assume a sample is packed if the main activity its manifest references cannot be found in the DEX + + DexClassLoader or Dex File This test is far from perfect """ smali_dir = os.path.join(self.outdir, 'smali') missing = False - if self.properties.manifest['main_activity'] != None: - filename = os.path.join(smali_dir, self.properties.manifest['main_activity'].replace('.', os.path.sep).replace('\'','') + '.smali') + if self.properties.manifest['main_activity'] is not None: + filename = os.path.join(smali_dir, self.properties.manifest['main_activity'].replace('.', os.path.sep) + .replace('\'', '') + '.smali') if not os.access(filename, os.R_OK): - if self.verbose: - print("Unable to find Main Activitity: {} filename={}".format(self.properties.manifest['main_activity'],filename)) + logging.debug("Unable to find Main Activitity: {} filename={}".format(self.properties.manifest['main_activity'], + filename)) missing = True if missing and (self.properties.smali['dex_class_loader'] or self.properties.smali['dex_file']): self.properties.smali['packed'] = True - def extract_manifest_properties(self): """Extracting services, receivers, activities etc from manifest""" @@ -504,14 +468,12 @@ try: xmldoc = xml.dom.minidom.parse(manifest) except expat.ExpatError: - if self.verbose: - print( "XML parsing error" ) - return; + logging.debug("XML parsing error") + return # getting package's name self.properties.manifest['package_name'] = xmldoc.documentElement.getAttribute('package') - tab = droidutil.get_elements(xmldoc, 'service', 'android:name') tab.extend(droidutil.get_elements(xmldoc, 'service', 'obfuscation:name')) for t in tab: @@ -523,21 +485,21 @@ tab.extend(droidutil.get_elements(xmldoc, 'receiver', 'obfuscation:name')) for t in tab: name = re.sub(r"u'(?P.*)'", r"\g", t) - if name != "''" : + if name != "''": self.properties.manifest['receivers'].append(name) tab = droidutil.get_elements(xmldoc, 'activity', 'android:name') tab.extend(droidutil.get_elements(xmldoc, 'activity', 'obfuscation:name')) for t in tab: name = re.sub(r"u'(?P.*)'", r"\g", t) - if name != "''" : + if name != "''": self.properties.manifest['activities'].append(name) tab = droidutil.get_elements(xmldoc, 'provider', 'android:name') tab.extend(droidutil.get_elements(xmldoc, 'provider', 'obfuscation:name')) for t in tab: name = re.sub(r"u'(?P.*)'", r"\g", t) - if name != "''" : + if name != "''": self.properties.manifest['providers'].append(name) tab = droidutil.get_elements(xmldoc, 'uses-library', 'android:name') @@ -564,22 +526,26 @@ droidutil.get_elements(xmldoc, 'meta-data', 'android:value') if self.verbose: - print( "MinSDK=%s MaxSDK=%s TargetSDK=%s" % (self.properties.manifest['minSDK'], self.properties.manifest['maxSDK'], self.properties.manifest['targetSDK'])) + logging.debug("MinSDK=%s MaxSDK=%s TargetSDK=%s" % (self.properties.manifest['minSDK'], + self.properties.manifest['maxSDK'], + self.properties.manifest['targetSDK'])) for perm in self.properties.manifest['permissions']: - print( "Requires permission " + perm) + logging.debug("Requires permission " + perm) # get main activity for actitem in xmldoc.getElementsByTagName('activity'): for a in actitem.getElementsByTagName('action'): - if a.getAttribute( 'android:name' ) == 'android.intent.action.MAIN' or a.getAttribute('obfuscation:name') == 'android.intent.action.MAIN': + if a.getAttribute( 'android:name') == 'android.intent.action.MAIN' \ + or a.getAttribute('obfuscation:name') == 'android.intent.action.MAIN': for b in actitem.getElementsByTagName('category'): - if b.getAttribute( 'android:name' ) == 'android.intent.category.LAUNCHER' or b.getAttribute('obfuscation:name') == 'android.intent.category.LAUNCHER': + if b.getAttribute('android:name') == 'android.intent.category.LAUNCHER'\ + or b.getAttribute('obfuscation:name') == 'android.intent.category.LAUNCHER': if actitem.getAttribute('android:name') != '': - self.properties.manifest['main_activity'] = actitem.getAttribute( 'android:name' ) + self.properties.manifest['main_activity'] = actitem.getAttribute('android:name') else: - self.properties.manifest['main_activity'] = actitem.getAttribute( 'obfuscation:name' ) - if self.verbose and self.properties.manifest['main_activity'] != None: - print( "Main activity: " + self.properties.manifest['main_activity']) + self.properties.manifest['main_activity'] = actitem.getAttribute('obfuscation:name') + if self.verbose and self.properties.manifest['main_activity'] is not None: + logging.debug("Main activity: " + self.properties.manifest['main_activity']) # search for swf metalist = droidutil.get_elements(xmldoc, 'meta-data', 'android:value') @@ -591,15 +557,13 @@ for r in xmldoc.getElementsByTagName('receiver'): for i in r.getElementsByTagName('intent-filter'): for a in i.getElementsByTagName('action'): - if a.getAttribute( 'android:name' ) == 'android.provider.Telephony.SMS_RECEIVED': + if a.getAttribute('android:name') == 'android.provider.Telephony.SMS_RECEIVED': self.properties.manifest['listens_incoming_sms'] = True - if a.getAttribute( 'android:name' ) == 'android.intent.action.NEW_OUTGOING_CALL': + if a.getAttribute('android:name') == 'android.intent.action.NEW_OUTGOING_CALL': self.properties.manifest['listens_outgoing_call'] = True - if self.verbose: - print( "Listens to incoming SMS : %d" % (self.properties.manifest['listens_incoming_sms'])) - print( "Listens to outgoing calls: %d" % (self.properties.manifest['listens_outgoing_call'])) - + logging.debug("Listens to incoming SMS : %d" % (self.properties.manifest['listens_incoming_sms'])) + logging.debug("Listens to outgoing calls: %d" % (self.properties.manifest['listens_outgoing_call'])) def extract_kit_properties(self): """ @@ -615,13 +579,12 @@ for pattern in pattern_list: for root, dirs, fname in os.walk(smali_dir): if pattern in root: - if self.verbose: - print("kits[%s] = True (detected pattern: %s)" % (section, pattern)) + logging.debug("kits[%s] = True (detected pattern: %s)" % (section, pattern)) list.append(section) - self.properties.kits[ section ] = True + self.properties.kits[section] = True break # break one level - if self.properties.kits[ section ] == True: - break # break another level + if self.properties.kits[section] is True: + break # break another level return list def extract_dex_properties(self): @@ -638,13 +601,12 @@ self.properties.dex['magic_unknown'] = True if magic[0:3] == 'dey': self.properties.dex['odex'] = True - for i in range(35,39): - if magic[4:7] == (b'0%d' % (i)): + for i in range(35, 39): + if magic[4:7] == (b'0%d' % i): self.properties.dex['magic'] = i self.properties.dex['magic_unknown'] = False - if self.verbose: - print( "DEX Magic: %s" % (repr(magic))) + logging.debug("DEX Magic: %s" % (repr(magic))) checksum = file.read(4) sha1 = file.read(20) @@ -655,38 +617,33 @@ if sha1.hex() != computed_sha1: self.properties.dex['bad_sha1'] = True - if self.verbose: - print( "DEX SHA1 read : %s" % sha1.hex()) - print( "DEX SHA1 computed: %s" % (computed_sha1)) + logging.debug("DEX SHA1 read : %s" % sha1.hex()) + logging.debug("DEX SHA1 computed: %s" % computed_sha1) else: - if self.verbose: - print( "Impossible to read file's SHA1 => impossible to check") + logging.debug("Impossible to read file's SHA1 => impossible to check") # check checksum if checksum != '': - file.seek(8+4, 0) # 0 = from beginning, skip magic and checksum - computed_adler32 = zlib.adler32(file.read()) & 0xffffffff # beware, adler32 returns an INTEGER - if computed_adler32 != struct.unpack(" impossible to check" ) + logging.debug("Impossible to read file's checksum => impossible to check") # check header size file.seek(8+4+20+4, 0) header_size = struct.unpack(" 0x70: - if self.verbose: - print( "DEX header is bigger than expected: %d (HoseDex2Jar?)" % (header_size) ) + logging.debug("DEX header is bigger than expected: %d (HoseDex2Jar?)" % header_size) self.properties.dex['big_header'] = True # look for 0 0x0 if-eq v0, v0, +9 - #1 0x4 fill-array-data v0, +3 (0x7) - #2 0xa fill-array-data-payload + # 1 0x4 fill-array-data v0, +3 (0x7) + # 2 0xa fill-array-data-payload # See http://www.dexlabs.org/blog/bytecode-obfuscation file.seek(0x68, 0) data_size = struct.unpack("= 0 and mykey.find('const-string') >=0 and match[mykey]: + if mykey.find('scp') >= 0 and mykey.find('const-string') >= 0 and match[mykey]: # const-string v[0-9]*, ".*scp.*" self.properties.smali['scp'] = True @@ -755,7 +708,7 @@ # let's not dump for nops if not (mykey == ' nop'): if match[mykey]: - analysis_file.write("## %s\n" % (mykey)) + analysis_file.write("## %s\n" % mykey) for element in match[mykey]: analysis_file.write("- "+str(element)+"\n") analysis_file.write('\n') @@ -764,8 +717,7 @@ # test if sample is likely to be packed self.is_packed() else: - if self.verbose: - print( "Cannot extract smali properties, because directory %s not found" % (smali_dir)) + logging.debug("Cannot extract smali properties, because directory %s not found" % smali_dir) # all smali properties should then be set to unknown for key in sorted(self.properties.smali.keys()): self.properties.smali[key] = 'unknown' @@ -774,14 +726,18 @@ """Will look for given properties (e.g GPS usage, presence of executables in all subdirectories (smali, assets, resources, library...)""" - if self.verbose: - print("------------- Extracting Wide properties") + logging.debug("------------- Extracting Wide properties") # detecting presence of ijiami packer ijiami = os.path.join(self.outdir, 'unzipped/assets/ijiami.dat') if os.access(ijiami, os.R_OK): self.properties.wide['ijiami'] = True + # detecting Flutter debug mode + flutter_debug = os.path.join(self.outdir, 'unzipped/assets/flutter_assets/kernel_blob.bin') + if flutter_debug: + self.properties.kits['flutter'] = True + # detect executables in resources self.find_exec_in_resources() @@ -806,7 +762,7 @@ self.properties.wideconfig.match_properties(match, self.properties.wide) - for mykey in match.keys(): # remember mykey is the matched value not the pattern + for mykey in match.keys(): # remember mykey is the matched value not the pattern if match[mykey]: analysis_file = open(os.path.join(self.outdir, droidlysis3.property_dump_file), 'a') analysis_file.write("- "+str(mykey)+"\n") @@ -820,11 +776,10 @@ # actually, it should be \+[0-9]{1,3}[0-9]{1,14} but that # generates too many false positives (numbers for other reasons) # so I'm being conservative - if mykey.startswith('+') and self.properties.wide['has_phonenumbers']: # mykey.startswith('+') and + if mykey.startswith('+') and self.properties.wide['has_phonenumbers']: # mykey.startswith('+') and if re.search(b"\+[0-9]{1,3}[0-9]{10,14}", mykey): self.properties.wide['phonenumbers'].append(mykey) - if self.verbose: - print( "Phone number spotted: " + mykey) + logging.debug("Phone number spotted: " + mykey) analysis_file = open(os.path.join(self.outdir, droidlysis3.property_dump_file), 'a') analysis_file.write("- "+str(mykey)+"\n") analysis_file.close() @@ -832,8 +787,7 @@ # we want to process each potential URL and IP address ip_pattern = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}") if ip_pattern.match(mykey): - if self.verbose: - print("IP address to check: {}".format(mykey)) + logging.debug("IP address to check: {}".format(mykey)) self.interesting_url(mykey) if mykey.find('http') >= 0 and match[mykey]: @@ -852,14 +806,14 @@ if os.access(strings_file, os.R_OK): xmldoc = xml.dom.minidom.parse(strings_file) sitems = xmldoc.getElementsByTagName('string') - if sitems != None: + if sitems is None: for item in sitems: if item.hasAttributes(): if item.getAttribute('name') == 'app_name': if item.hasChildNodes(): self.properties.app_name = item.childNodes[0].data # rule out chinese characters: TODO: convert chinese characters to printable - self.properties.app_name = re.sub('[^:print:]','', self.properties.app_name) + self.properties.app_name = re.sub('[^:print:]', '', self.properties.app_name) break def extract_arm_properties(self, arm_filename=''): @@ -870,8 +824,7 @@ if self.properties.filetype == droidutil.ARM or arm_filename != '': if arm_filename == '': arm_filename = self.absolute_filename - if self.verbose: - print( "Extracting properties from " + arm_filename) + logging.debug("Extracting properties from " + arm_filename) # Run "strings" on the executable proc = subprocess.Popen(['strings', arm_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -879,10 +832,9 @@ for section in self.properties.armconfig.get_sections(): matches = re.findall(self.properties.armconfig.get_pattern(section), output[0].decode('utf-8')) - if matches != None and len(matches) > 0: + if matches is not None and len(matches) > 0: self.properties.arm[section] = True - if self.verbose: - print( "Setting arm[%s] = True" % (section)) + logging.debug("Setting arm[%s] = True" % section) if section == 'url_in_exec': for m in matches: self.interesting_url(m) @@ -902,35 +854,35 @@ absolute_file = os.path.join(dir, file) if os.path.isfile(absolute_file): filetype = droidutil.get_filetype(absolute_file) + + # Special case to detect Flutter apps compiled in release mode + if file == 'libapp.so': + self.properties.kits['flutter'] = True + if filetype == droidutil.ARM: - if self.verbose: - print( "%s is an ARM executable" % (absolute_file)) + logging.debug("%s is an ARM executable" % absolute_file) found_arm.append(absolute_file) else: if filetype == droidutil.ZIP: innerzip = droidziprar.droidziprar(absolute_file, True, self.verbose) - if innerzip.handle == None: - if self.verbose: - print( "%s is not a valid zip" % (absolute_file)) + if innerzip.handle is None: + logging.debug("%s is not a valid zip" % absolute_file) return found_apk, found_arm filetype = innerzip.get_type() innerzip.close() if filetype == droidutil.APK: - if self.verbose: - print( "%s contains an APK" % (absolute_file)) + logging.debug("%s contains an APK" % absolute_file) found_apk.append(absolute_file) else: if filetype == droidutil.RAR: innerzip = droidziprar.droidziprar(absolute_file, False, self.verbose) - if innerzip.handle == None: - if self.verbose: - print( "%s is not a valid rar" % (absolute_file)) + if innerzip.handle is None: + logging.debug("%s is not a valid rar" % absolute_file) return found_apk, found_arm filetype = innerzip.get_type() innerzip.close() if filetype == droidutil.APK: - if self.verbose: - print( "%s contains an APK" % (absolute_file)) + logging.debug("%s contains an APK" % absolute_file) found_apk.append(absolute_file) return found_arm, found_apk @@ -941,28 +893,26 @@ lib_dir = os.path.join(self.outdir, "lib/armeabi") lib2_dir = os.path.join(self.outdir, "lib/arm64-v8a") - list_dir = [ asset_dir, raw_dir, lib_dir, lib2_dir ] + list_dir = [asset_dir, raw_dir, lib_dir, lib2_dir] for dir in list_dir: if os.access(dir, os.R_OK) and os.path.isdir(dir): - if self.verbose: - print( "Parsing %s for embedded executables or zips... " % (dir)) + logging.debug("Parsing %s for embedded executables or zips... " % dir) found_arm, found_apk = self.find_executables(dir) if found_arm or found_apk: self.properties.wide['embed_exec'] = True - if self.verbose: - print( "Embedded executables/zip found in " + dir) + logging.debug("Embedded executables/zip found in " + dir) if found_arm: for arm in found_arm: - if self.verbose: - print( "Recursively processing " + arm) + logging.debug("Recursively processing " + arm) self.extract_arm_properties(arm) if found_apk: for apk in found_apk: - if self.verbose: - print( "Recursively processing " + apk) - droidlysis3.process_file(apk, self.outdir, self.verbose, self.clear, self.enable_procyon, self.disable_description, self.disable_dump, self.no_kit_exception) + logging.debug("Recursively processing " + apk) + droidlysis3.process_file(apk, self.outdir, self.verbose, self.clear, self.enable_procyon, + disable_report=self.disable_description, + no_kit_exception=self.no_kit_exception) def interesting_url(self, url): """Rules out meaningless URLs to keep only those which might be useful for attackers. @@ -971,41 +921,41 @@ output: self.properties.url list Will set self.properties. urls and some wide properties. """ - assert url != None, "Empty URL is not valid" - url = re.sub('[,;" ].*', '', url) # remove after ",; or whitespace - url = re.sub('\n.*','', url) + assert url is not None, "Empty URL is not valid" + url = re.sub('[,;" ].*', '', url) # remove after ",; or whitespace + url = re.sub('\n.*', '', url) url = re.sub('\r.*', '', url) - url = re.sub('[^\x20-\x7e]','', url) + url = re.sub('[^\x20-\x7e]', '', url) url_regexp = '|'.join(droidurl.build_special_url_list()) - match = re.search(url_regexp, url) # only one match per line + match = re.search(url_regexp, url) # only one match per line - if match == None: + if match is None: # the url is not in the list of special URLs # let's check it hasn't been reported yet (unique URLs) if url not in self.properties.wide['urls']: self.properties.wide['urls'].append(url) - if self.verbose: - print( "URL: %s" % (url)) + logging.debug("URL: %s" % url) # raise a warning if it downloads APKs or for dropbox - search = re.findall("\.apk|\.zip",url) - if search != None and len(search) > 0: + search = re.findall("\.apk|\.zip", url) + if search is not None and len(search) > 0: if '\.apk' in search or '\.zip' in search: self.properties.wide['apk_zip_url'] = True return self.properties.wide['urls'] def find_base64_strings(self, thedir, exceptions, theproperty): - ''' + """ To detect base64 strings, we spot all constants with potentially base64 encoded characters. Then, on these, we attempt to perform base 64 decoding. If that does not lead to an error (padding exception etc) and if the result is printable (~ makes potential sense), we assume this is a Base64 string - ''' - # const-string v1, "xyz==" - # this won't detect all base64 strings because they won't all finish with == - # but we'll get some... + + const-string v1, "xyz==" + this won't detect all base64 strings because they won't all finish with == + but we'll get some... + """ base64_regexp = bytes("const-string v[0-9]*, \"[a-zA-Z0-9/?=]*\"", 'utf-8') base64_strings = droidutil.recursive_search(base64_regexp, thedir, exceptions, False) @@ -1014,7 +964,7 @@ for s in base64_strings.keys(): # remove the const-string vX part and trailing " thestr = re.sub('const-string v[0-9]*, "', '', s) - thestr = re.sub('"','', thestr) + thestr = re.sub('"', '', thestr) # try to decode the Base64 string try: diff -Nru droidlysis-3.4.0/droidsql.py droidlysis-3.4.1/droidsql.py --- droidlysis-3.4.0/droidsql.py 2021-08-19 09:28:51.000000000 +0000 +++ droidlysis-3.4.1/droidsql.py 2023-02-21 14:30:46.000000000 +0000 @@ -14,6 +14,7 @@ engine = create_engine(droidconfig.SQLALCHEMY, echo=verbose) Base = declarative_base() + class Sample(Base): __tablename__ = 'samples' @@ -33,10 +34,8 @@ kits = Column(String()) def __repr__(self): - return "" % (self.sha256) + return "" % self.sha256 Base.metadata.create_all(engine) - - diff -Nru droidlysis-3.4.0/droidurl.py droidlysis-3.4.1/droidurl.py --- droidlysis-3.4.0/droidurl.py 2021-10-25 07:26:16.000000000 +0000 +++ droidlysis-3.4.1/droidurl.py 2023-02-21 14:30:46.000000000 +0000 @@ -29,11 +29,14 @@ list.append('^http://server/.*') list.append('username:password@YOUR') list.append('www\.dummyurl\.com') - - + list.append('www\.example\.com') # clean URLs - which do not correspond to a kit i.e with a smali path list.append('creativecommons\.org') list.append('docs\.google\.') + list.append('support\.google\.') + list.append('developer\.android\.com') + list.append('developers\.google\.com') + list.append('googlesyndication\.com') list.append('jsoup\.org') list.append('www\.jcip\.net') list.append('finance\.google\.') @@ -42,6 +45,7 @@ list.append('https*://[a-zA-Z]*\.google\.com/') list.append('www\.google\.') list.append('checkout\.google\.com') + list.append('doubleclick\.net') list.append('\.google-analytics\.com') list.append('^https*://[a-z]*\.googleapis\.com/') list.append('plus\.url\.google\.com') @@ -50,8 +54,11 @@ list.append('material\.io') list.append('java\.sun\.com') list.append('\.facebook\.com/help') - list.append('\.facebook\.com$') + list.append('\.facebook\.com$') + list.append('fonts\.gstatic\.com') list.append('forum\.xda-developers\.com/showthread\.php') + list.append('www\.freetype\.org') + list.append('www\.microsoft\.com') list.append('mozilla\.org') list.append('www\.android\.com') list.append('developer\.android\.com/reference/') @@ -85,9 +92,11 @@ list.append('github\.com') list.append('jquery\.org') list.append('www\.iana\.org') + list.append('stackoverflow\.com') + list.append('www\.unicode\.org') + list.append('freetype\.org') - - # AV + # AV list.append('www\.fortinet\.com') list.append('docs\.fortinet\.com/fclient/android/') list.append('home\.mcafee\.com') @@ -98,7 +107,7 @@ list.append('www\.trendmicro\.com') list.append('https*://[a-zA-Z0-9]*\.360safe\.com') - # search engines + # search engines list.append('search\.twitter\.com') list.append('search\.yahoo\.com') list.append('www\.baidu\.com') @@ -107,7 +116,7 @@ list.append('www\.searchmobileonline\.com') - # XML + # XML list.append('^https*://push$') list.append('^https*://schemas') list.append('^https*://www\.$') @@ -117,7 +126,7 @@ list.append('xmlpull\.org') list.append('^https*://.*/configure[-_0-9]*\.dtd$') - # Operator + # Operator list.append('10\.0\.0\.172') list.append('10\.0\.0\.200') list.append('wap\.uni-info\.com\.cn') @@ -139,5 +148,3 @@ list.append('^https*://.*\.alipay.com') return list - - diff -Nru droidlysis-3.4.0/droidutil.py droidlysis-3.4.1/droidutil.py --- droidlysis-3.4.0/droidutil.py 2021-08-19 09:28:51.000000000 +0000 +++ droidlysis-3.4.1/droidutil.py 2023-02-21 14:43:02.000000000 +0000 @@ -3,24 +3,28 @@ import re import shutil import magic -import xml.dom.minidom import hashlib from collections import defaultdict """Those are my own utilities for sample analysis""" + def mkdir_if_necessary(path): - """Creates the directory if it does not exist yet. + """ + Creates the directory if it does not exist yet. If it exists, does not do anything. - If path is None (not filled), does not do anything.""" + If path is None (not filled), does not do anything. + """ - if path != None: + if path is not None: try: os.makedirs(path) - except OSError as exc: # Python >2.5 + except OSError as exc: # Python >2.5 if exc.errno == errno.EEXIST and os.path.isdir(path): pass - else: raise + else: + raise + def on_rm_tree_error(fn, path, exc_info): """ @@ -38,11 +42,13 @@ os.chmod(path, 777) os.remove(path) -def move_dir(src,dst): - """Move src directory to dst - works even if dst already exists.""" - assert os.path.isdir(src), "src must be an existing directory" - os.system ("mv"+ " " + src + "/* " + dst) - shutil.rmtree(src, onerror=on_rm_tree_error) + +def move_dir(src, dst): + # Move src directory to dst - works even if dst already exists. + assert os.path.isdir(src), "src must be an existing directory" + os.system("mv" + " " + src + "/* " + dst) + shutil.rmtree(src, onerror=on_rm_tree_error) + def sanitize_filename(filename): """Sanitizes a filename so that we can create the output analysis directory without any problem. @@ -52,21 +58,21 @@ Returns the sanitized name.""" # we remove any character which is not letters, numbers, _ or . - return re.sub('[^a-zA-Z0-9_\.]','', filename) + return re.sub('[^a-zA-Z0-9_\.]', '', filename) + def listAll(dirName): - filelist1=[] + filelist1 = [] files = os.listdir(dirName) for f in files: - if os.path.isfile(os.path.join(dirName,f)): - filelist1.append(os.path.join(dirName,f)) + if os.path.isfile(os.path.join(dirName, f)): + filelist1.append(os.path.join(dirName, f)) else: - newlist=listAll(os.path.join(dirName,f)); + newlist = listAll(os.path.join(dirName, f)) filelist1.extend(newlist) return filelist1 - def count_filedirs(dirname): """Counts the number of directories and files in a given directory. Counts recursively. dirname must be readable. @@ -94,10 +100,11 @@ return nb_dirs, nb_files + def sha256sum(input_file_name): """Computes the SHA256 hash of a binary file Returns the digest string or '' if an error occurred reading the file""" - chunk_size = 1048576 # 1 MB + chunk_size = 1048576 # 1 MB file_sha256 = hashlib.sha256() try: with open(input_file_name, "rb") as f: @@ -106,14 +113,17 @@ file_sha256.update(byte) byte = f.read(chunk_size) except IOError: - print ('sha256sum: cannot open file: %s' % (input_file_name)) + print('sha256sum: cannot open file: %s' % input_file_name) return '' return file_sha256.hexdigest() + def sha1sum(input_file_name): - """Computes the SHA1 hash of a binary file - Returns the digest string or '' if an error occurred reading the file""" - chunk_size = 1048576 # 1 MB + """ + Computes the SHA1 hash of a binary file + Returns the digest string or '' if an error occurred reading the file + """ + chunk_size = 1048576 # 1 MB file_sha1 = hashlib.sha1() try: with open(input_file_name, "rb") as f: @@ -122,34 +132,36 @@ file_sha1.update(byte) byte = f.read(chunk_size) except IOError: - print ('sha1sum: cannot open file: %s' % (input_file_name)) + print('sha1sum: cannot open file: %s' % input_file_name) return '' return file_sha1.hexdigest() + # -------------------------- File Constants ------------------------- """Something else than the other file types. We do not support this file type.""" -UNKNOWN=0 +UNKNOWN = 0 """An APK. It is not possible to differentiate a ZIP from an APK until we have looked inside the ZIP.""" -APK=1 +APK = 1 """A Dalvik Executable file. We do not check the file is valid/accepted by the verifier.""" -DEX=2 +DEX = 2 """An ARM ELF executable.""" -ARM=3 +ARM = 3 """A Java .class file""" -CLASS=4 +CLASS = 4 """A Zip file. Actually, this can also be a JAR or an APK until we have thoroughly checked.""" -ZIP=5 +ZIP = 5 """A RARed file.""" -RAR=6 +RAR = 6 """We can probably add some more later: TAR, TGZ, BZ2...""" + def str_filetype(filetype): """Provide as input a droidutil filetype (APK, DEX, ARM...) and returns the corresponding string""" if filetype == APK: @@ -179,22 +191,25 @@ droidutil.UNKNOWN """ filetype = magic.from_file(filename) - if filetype == None: + if filetype is None: # this happens if magic is unable to find file type return UNKNOWN - match = re.search('Zip archive data|zip|RAR archive data|executable, ARM|shared object, ARM|Java class|Dalvik dex|Java archive', filetype) - if match == None: + match = re.search('Zip archive data|zip|RAR archive data|executable, ARM|' + 'shared object, ARM|Java class|Dalvik dex|Java archive|Android package', filetype) + if match is None: mytype = UNKNOWN else: - typecase = { 'Zip archive data' : ZIP, - 'zip' : ZIP, - 'Java archive' : ZIP, - 'RAR archive data' : RAR, - 'executable, ARM' : ARM, - 'shared object, ARM' : ARM, - 'Java class' : CLASS, - 'Dalvik dex' : DEX, - 'None' : UNKNOWN } + typecase = {'Zip archive data': ZIP, + 'zip': ZIP, + 'Java archive': ZIP, + 'RAR archive data': RAR, + 'executable, ARM': ARM, + 'shared object, ARM': ARM, + 'Java class': CLASS, + 'Dalvik dex': DEX, + 'Android package': ZIP, # droidsample needs ZIP type to do all the processing + 'None': UNKNOWN + } mytype = typecase[match.group(0)] return mytype @@ -202,20 +217,24 @@ def get_elements(xmldoc, tag_name, attribute): """Returns a list of elements""" l = [] - for item in xmldoc.getElementsByTagName(tag_name) : + for item in xmldoc.getElementsByTagName(tag_name): value = item.getAttribute(attribute) - l.append( repr( value ) ) + l.append(repr(value)) return l + def get_element(xmldoc, tag_name, attribute): - for item in xmldoc.getElementsByTagName(tag_name) : + for item in xmldoc.getElementsByTagName(tag_name): value = item.getAttribute(attribute) - if len(value) > 0 : + if len(value) > 0: return value return None + """Very simple exception to raise when we found something. For instance to break a loop.""" -class Found(Exception): pass +class Found(Exception): + pass + class matchresult: """Match information""" @@ -231,11 +250,8 @@ return 'file=%s lineno=%d line=%s' % (self.file, self.lineno, self.line) def __str__(self): - if len(self.file) > 70: - f = '...'+self.file[-70:] - else: - f = self.file - return 'file=%50s no=%4d line=%30s' % (f, self.lineno, self.line) + return 'file=%s no=%4d line=%30s' % (self.file, self.lineno, self.line) + def recursive_search(search_regexp, directory, exception_list=[], verbose=False): """Recursively search in a directory except in some subdirectories @@ -260,10 +276,10 @@ if os.path.isfile(current_entry): for exception in exception_list: # TO DO: not entirely sure we need 'match'? perhaps if it is a regexp? - # Remember that "exception" can be part of a path e.g we want everything that matches blah/bloh + # Remember that "exception" can be part of a path e.g. we want everything that matches blah/bloh # then com/blah/bloh must match - match = re.search(exception, current_entry) # TO DO: not entirely sure we need the match - if match != None or exception in current_entry: + match = re.search(exception, current_entry) # TO DO: not entirely sure that we need the match + if match is not None or exception in current_entry: # skip this file raise Found @@ -272,19 +288,20 @@ for line in open(current_entry, 'rb'): lineno += 1 match = re.search(search_regexp, line) - if match != None: + if match is not None: if verbose: - print("Match: File: " +entry+ " Keyword: " +match.group(0).decode('utf-8', errors='replace') + " Line: " + line.decode('utf-8', errors='replace')) + print("Match: File: " + entry + " Keyword: " + + match.group(0).decode('utf-8', errors='replace') + + " Line: " + line.decode('utf-8', errors='replace')) """match.group(0) only provides one match per line if we need more, re.search is not appropriate and should be replaced by re.findall""" - matches[ match.group(0).decode('utf-8', errors='replace') ].append(matchresult(current_entry, line, lineno)) - + matches[match.group(0).decode('utf-8', errors='replace')].append(matchresult(current_entry, line, lineno)) if os.path.isdir(current_entry): for exception in exception_list: match = re.search(exception, current_entry) - if match != None: + if match is not None: # skip this directory raise Found @@ -293,14 +310,12 @@ hismatches = recursive_search(search_regexp, current_entry, exception_list, verbose) # merge in those results for key in hismatches.keys(): - matches[ key ].extend( hismatches[ key ] ) + matches[key].extend(hismatches[key]) except RuntimeError: # we get this when there are too many recursive dirs - pass # next - + pass # next except Found: - pass # go to next entry + pass # go to next entry return matches - diff -Nru droidlysis-3.4.0/droidziprar.py droidlysis-3.4.1/droidziprar.py --- droidlysis-3.4.0/droidziprar.py 2022-01-18 08:30:20.000000000 +0000 +++ droidlysis-3.4.1/droidziprar.py 2023-02-21 14:43:02.000000000 +0000 @@ -6,18 +6,23 @@ __license__ = "MIT License" """ import zipfile -import rarfile # install from https://github.com/markokr/rarfile +import rarfile import re import droidutil import struct import subprocess +import logging + +logging.basicConfig(format='%(levelname)s:%(filename)s:%(message)s', level=logging.INFO) + class droidziprar: def __init__(self, archive, zipmode=True, verbose=False): """Returns the archive file handle - don't forget to close it once you've finished with the file""" - self.verbose = verbose - self.zipmode = zipmode # True for a zip, False for a Rar + if verbose: + logging.getLogger().setLevel(logging.DEBUG) + self.zipmode = zipmode # True for a zip, False for a Rar self.password = 'infected' self.handle = None self.archive_name = archive @@ -30,21 +35,17 @@ self.open(archive) - def open(self, filename): try: if self.zipmode: - if self.verbose: - print( "Opening Zip archive "+filename) + logging.debug("Opening Zip archive "+filename) self.handle = zipfile.ZipFile(filename, 'r') else: - if self.verbose: - print( "Opening Rar archive "+filename) + logging.debug("Opening Rar archive "+filename) self.handle = rarfile.RarFile(filename, 'r') except (struct.error, zipfile.BadZipfile, zipfile.LargeZipFile, IOError) as e: - if self.verbose: - print( "Exception caught in ZipFile: %s" % (repr(e))) + logging.debug("Exception caught in ZipFile: %s" % (repr(e))) self.handle = None return self.handle @@ -56,9 +57,9 @@ - the file type: APK, CLASS or ZIP - a list of inner zips/rars if any """ - assert self.handle != None, "zip/rar file handle has been closed" + assert self.handle is not None, "zip/rar file handle has been closed" - innerzips = [] # default value + innerzips = [] # default value files_in_zip = self.handle.namelist() # guess file type based on contents @@ -84,27 +85,26 @@ """ if self.zipmode: # the unzip command works better... (manages to unzip more) - subprocess.call([ "/usr/bin/unzip" , "-o", "-qq", "-P", self.password, "-d", outdir, self.archive_name, filename]) + subprocess.call(["/usr/bin/unzip", "-o", "-qq", "-P", + self.password, "-d", outdir, self.archive_name, filename]) else: - assert self.handle != None, "zip/rar file handle has been closed" + assert self.handle is not None, "zip/rar file handle has been closed" # beware this may raise errors KeyError, RuntimeError... self.handle.extract(filename, outdir, pwd=self.password) - - def extract_all(self, outdir): if self.zipmode: - print( 'Launching unzip process on %s with output dir=%s' % (self.archive_name, outdir)) - subprocess.call([ "/usr/bin/unzip" , "-o", "-qq", "-P", self.password, self.archive_name, "-d", outdir ]) + print('Launching unzip process on %s with output dir=%s' % (self.archive_name, outdir)) + subprocess.call(["/usr/bin/unzip", "-o", "-qq", "-P", self.password, self.archive_name, "-d", outdir]) else: - assert self.handle != None, "zip/rar file handle has been closed" + assert self.handle is not None, "zip/rar file handle has been closed" self.handle.extractall(path=outdir, pwd=self.password) def extract_pattern(self, outdir, pattern): """Unzips (or unrars) files matching a given regexp to a given output directory. Returns a list of what has been unzipped. """ - assert self.handle != None, "zip/rar file handle has been closed" + assert self.handle is not None, "zip/rar file handle has been closed" all_files = self.handle.namelist() list = [x for x in all_files if re.search(pattern, x)] @@ -115,20 +115,18 @@ def get_date(self, filename): """Gets the time at which filename was created""" - assert self.handle != None, "zip/rar file handle has been closed" + assert self.handle is not None, "zip/rar file handle has been closed" try: metainfo = self.handle.getinfo(filename) return metainfo.date_time except (KeyError, rarfile.NoRarEntry) as e: - if self.verbose: - print( "%s does not exist in %s" % (filename, self.archive_name)) + logging.debug("%s does not exist in %s" % (filename, self.archive_name)) return None def close(self): - if self.handle == None: + if self.handle is not None: pass else: self.handle.close() - if self.verbose: - print( "Closing archive: " + self.archive_name) + logging.debug("Closing archive: " + self.archive_name) diff -Nru droidlysis-3.4.0/PKG-INFO droidlysis-3.4.1/PKG-INFO --- droidlysis-3.4.0/PKG-INFO 2022-01-18 12:44:52.427216800 +0000 +++ droidlysis-3.4.1/PKG-INFO 2023-02-21 14:47:48.904115400 +0000 @@ -1,105 +1,91 @@ Metadata-Version: 2.1 Name: droidlysis -Version: 3.4.0 -Summary: DroidLysis: pre-analysis script for suspicious Android samples +Version: 3.4.1 +Summary: DroidLysis: pre-analysis of suspicious Android samples Home-page: https://github.com/cryptax/droidlysis Author: @cryptax Author-email: aafortinet@gmail.com License: MIT Keywords: android malware reverse -Platform: UNKNOWN Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License Classifier: Development Status :: 3 - Alpha Classifier: Operating System :: Unix Classifier: Topic :: Software Development :: Disassemblers -Requires-Python: >=3.0.* +Requires-Python: >=3.0 Description-Content-Type: text/markdown License-File: LICENSE # DroidLysis -DroidLysis is a **property extractor for Android apps**. -It automatically disassembles the Android application you provide -and looks for various properties within the package or its disassembly. +DroidLysis is a **pre-analysis tool for Android apps**: it performs repetitive and boring tasks we'd typically do at the beginning of any reverse engineering. It disassembles the Android sample, organizes output in directories, and searches for suspicious spots in the code to look at. +The output helps the reverse engineer speed up the first few steps of analysis. DroidLysis can be used over Android packages (apk), Dalvik executables (dex), Zip files (zip), Rar files (rar) or directories of files. - + ## Quick setup Can't wait to use DroidLysis? Then, use a Docker container: ``` -$ docker pull cryptax/droidlysis:2021.04 -$ docker run -it --rm -v /tmp/share:/share cryptax/droidlysis:2021.04 /bin/bash +$ docker pull cryptax/droidlysis:2023.02 +$ docker run -it --rm -v /tmp/share:/share cryptax/droidlysis:2023.02 /bin/bash +$ cd /opt/droidlysis +$ python3 ./droidlysis3.py --help ``` -DroidLysis is located in `/opt/droidlysis`. - ## Installing DroidLysis 1. Install required system packages 2. Install Android disassembly tools -3. Get DroidLysis from the Git repository or from pip +3. Get DroidLysis from the Git repository (preferred) or from pip 4. Configure `droidconfig.py` -### Step1: Install required system packages - -`sudo apt-get install default-jre git python3 python3-pip unzip wget libmagic-dev libxml2-dev libxslt-dev` - -### Step 2: Install Android disassembly tools - -DroidLysis does not perform the disassembly itself, but relies on other tools to do so. Therefore, you must install: - -- [Apktool](https://ibotpeaches.github.io/Apktool/) - note we only need the Jar. -- [Baksmali](https://bitbucket.org/JesusFreke/smali/downloads) - note we only need the Jar. +Install required system packages: -Optionally: - -- [Dex2jar](https://github.com/pxb1988/dex2jar) - dex2jar is now *optional*. If you don't need Dex to Jar transformation (useful for later decompiling!), you can skip it. -- [Procyon](https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler) - *optional*. If you don't want to use this decompiler, skip its installation. - -Some of these tools are redundant, but sometimes one fails on a sample while another does not. DroidLysis detects this and tries to switch to a tool that works for the sample. +``` +sudo apt-get install default-jre git python3 python3-pip unzip wget libmagic-dev libxml2-dev libxslt-dev +``` -For example, +Install Android disassembly tools: [Apktool](https://ibotpeaches.github.io/Apktool/) , +[Baksmali](https://bitbucket.org/JesusFreke/smali/downloads), and optionally +[Dex2jar](https://github.com/pxb1988/dex2jar) and +[Procyon](https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler) (note that Procyon only works with Java 8, not Java 11). ``` $ mkdir -p ~/softs $ cd ~/softs -$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.6.0.jar +$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.7.0.jar $ wget https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar -$ wget https://github.com/pxb1988/dex2jar/files/1867564/dex-tools-2.1-SNAPSHOT.zip -$ unzip dex-tools-2.1-SNAPSHOT.zip -$ rm -f dex-tools-2.1-SNAPSHOT.zip +$ wget https://github.com/pxb1988/dex2jar/releases/download/v2.2-SNAPSHOT-2021-10-31/dex-tools-2.2-SNAPSHOT-2021-10-31.zip +$ unzip dex-tools-2.2-SNAPSHOT-2021-10-31.zip +$ rm -f dex-tools-2.2-SNAPSHOT-2021-10-31.zip ``` -### Step 3a. Get DroidLysis from the Git Repository - -This is the most up-to-date version. - +Install from Git in a Python virtual environment: ``` -$ git clone https://github.com/cryptax/droidlysis -$ cd droidlysis -$ pip3 install -r requirements.txt +$ python3 -m venv venv +$ source ./venv/bin/activate +(venv) $ pip3 install git+https://github.com/cryptax/droidlysis ``` -### Step 3b. Get DroidLysis from Pypi - -Alternatively, you can install DroidLysis from pip3. Note the package may be slightly behind the git repository. +Run it: ``` -$ python3 -m venv droidlysis -$ cd droidlysis -$ source ./bin/activate -(droidlysis) /droidlysis # pip3 install droidlysis +cd droidlysis +./droidlysis --help ``` -### Step 4. Configure `droidconfig.py` +Alternatively, you can install DroidLysis directly from PyPi (`pip3 install droidlysis`). + +## Configuration -The configuration is extremely simple, you only need to tune `droidconfig.py`: +If you used the default install commands & directories as specified above, you won't need any configuration. + +The configuration is extremely simple, you only need to tune `droidconfig.py`. Note that if you placed the tools in the default `~/softs` directory as I specified, you don't have to do anything: the tools will be automatically found in that location. - `APKTOOL_JAR`: set the path to your apktool jar - `BAKSMALI_JAR`: set the path to your baksmali jar @@ -107,18 +93,19 @@ - `PROCYON_JAR`: set the path to the procyon decompiler jar. If you don't want Procyon, leave this path to a non existant file. - `INSTALL_DIR`: set the path to your DroidLysis instance. Do not forget to set this or DroidLysis won't work correctly! -Example: +By default, `droidconfig.py` searches for tools at the following location: ```python -APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.5.0.jar") +APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.7.0.jar") BAKSMALI_JAR = os.path.join(os.path.expanduser("~/softs"), "baksmali-2.5.2.jar") -DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.s -h") +DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.sh") PROCYON_JAR = os.path.join( os.path.expanduser("~/softs"), "procyon-decompiler-0.5.36.jar") INSTALL_DIR = os.path.expanduser("~/droidlysis") ``` -Optionally, if you need a specific situation, you might need to tune the following too. Normally, the default options will work and you won't have to touch these: +Optionally, if you need a specific situation, you might need to tune the following too. +Normally, the default options will work and you won't have to touch these: + - `SQLALCHEMY`: specify your SQL database. - `KEYTOOL`: absolute path of `keytool` which generally ships with Java - `SMALI_CONFIGFILE`: smali patterns @@ -132,34 +119,32 @@ DroidLysis uses **Python 3**. To launch it and get options: ``` -python3 ./droidlysis3.py --help +droidlysis --help ``` For example, test it on [Signal's APK](https://signal.org/android/apk/): ``` -python3 ./droidlysis3.py --input Signal-website-universal-release-4.52.4.apk --output /tmp +droidlysis --input Signal-website-universal-release-4.52.4.apk --output /tmp ``` -![](./example.png) +![](./images/example.png) DroidLysis outputs: -- A summary on the console (see example.png) +- A summary on the console (see image above) - The unzipped, pre-processed sample in a subdirectory of your output dir. The subdirectory is named using the sample's filename and sha256 sum. For example, if we analyze the Signal application and set `--output /tmp`, the analysis will be written to `/tmp/Signalwebsiteuniversalrelease4.52.4.apk-f3c7d5e38df23925dd0b2fe1f44bfa12bac935a6bc8fe3a485a4436d4487a290`. - A database (by default, SQLite `droidlysis.db`) containing properties it noticed. ## Options -Get usage with `python3 ./droidlysis3.py --help` +Get usage with `droidlysis --help` - The input can be a file or a directory of files to recursively look into. DroidLysis knows how to process Android packages, DEX, ODEX and ARM executables, ZIP, RAR. DroidLysis won't fail on other type of files (unless there is a bug...) but won't be able to understand the content. -- When processing directories of files, it is typically quite helpful to move processed samples to another location to know what has been processed. This is handled by option `--movein`. Also, if you are only interested in statistics, you should probably clear the output directory which contains detailed information for each sample: this is option `--clearoutput`. +- When processing directories of files, it is typically quite helpful to move processed samples to another location to know what has been processed. This is handled by option `--movein`. Also, if you are only interested in statistics, you should probably clear the output directory which contains detailed information for each sample: this is option `--clearoutput`. If you want to store all statistics in a SQL database, use `--enable-sql` (see [here](#sqlite_database)) -- When dealing with single samples, on the contrary, statistics are typically not so interesting, and their generation can be disabled with `--disable-sql` - -- DEX decompilation is quite long with Procyon, so this option is disabled by default. If you want to decompile to Java, use `--enable-procyon`. +- DEX decompilation is quite long with Procyon, so this option is *disabled* by default. If you want to decompile to Java, use `--enable-procyon`. - DroidLysis's analysis does not inspect known 3rd party SDK by default, i.e. for instance it won't report any suspicious activity from these. If you want them to be inspected, use option `--no-kit-exception`. This usually creates many more detected properties for the sample, as SDKs (e.g. advertisment) use lots of flagged APIs (get GPS location, get IMEI, get IMSI, HTTP POST...). @@ -182,7 +167,7 @@ If you do not need the sample output directory to be generated, use the option `--clearoutput`. -## SQLite database +## SQLite database{#sqlite_database} If you want to process a directory of samples, you'll probably like to store the properties DroidLysis found in a database, to easily parse and query the findings. In that case, use the option `--enable-sql`. This will automatically dump all results in a database named `droidlysis.db`, in a table named `samples`. Each entry in the table is relative to a given sample. Each column is properties DroidLysis tracks. @@ -210,22 +195,18 @@ description=Sending SMS messages ``` - ## To do -- The code is quite crappy now. I could probably do the same in less lines! -- Replace print by logging -- Remove the "caution: filename not matched: classes6.dex" which occurs at file extraction in droidsample.py +- Remove the "caution: filename not matched: classes6.dex" which occurs at file extraction in `droidsample.py` ## Updates -v3.4.0 - Multidex support -v3.3.1 - Improving detection of Base64 strings -v3.3.0 - Dumping data to JSON -v3.2.1 - IP address detection -v3.2.0 - Dex2jar is optional -v3.1.0 - Detection of Base64 strings - - +- v3.4.1 - Removed dependency to Androguard +- v3.4.0 - Multidex support +- v3.3.1 - Improving detection of Base64 strings +- v3.3.0 - Dumping data to JSON +- v3.2.1 - IP address detection +- v3.2.0 - Dex2jar is optional +- v3.1.0 - Detection of Base64 strings diff -Nru droidlysis-3.4.0/README.md droidlysis-3.4.1/README.md --- droidlysis-3.4.0/README.md 2022-01-18 10:08:05.000000000 +0000 +++ droidlysis-3.4.1/README.md 2023-02-21 14:43:02.000000000 +0000 @@ -1,86 +1,73 @@ # DroidLysis -DroidLysis is a **property extractor for Android apps**. -It automatically disassembles the Android application you provide -and looks for various properties within the package or its disassembly. +DroidLysis is a **pre-analysis tool for Android apps**: it performs repetitive and boring tasks we'd typically do at the beginning of any reverse engineering. It disassembles the Android sample, organizes output in directories, and searches for suspicious spots in the code to look at. +The output helps the reverse engineer speed up the first few steps of analysis. DroidLysis can be used over Android packages (apk), Dalvik executables (dex), Zip files (zip), Rar files (rar) or directories of files. - + ## Quick setup Can't wait to use DroidLysis? Then, use a Docker container: ``` -$ docker pull cryptax/droidlysis:2021.04 -$ docker run -it --rm -v /tmp/share:/share cryptax/droidlysis:2021.04 /bin/bash +$ docker pull cryptax/droidlysis:2023.02 +$ docker run -it --rm -v /tmp/share:/share cryptax/droidlysis:2023.02 /bin/bash +$ cd /opt/droidlysis +$ python3 ./droidlysis3.py --help ``` -DroidLysis is located in `/opt/droidlysis`. - ## Installing DroidLysis 1. Install required system packages 2. Install Android disassembly tools -3. Get DroidLysis from the Git repository or from pip +3. Get DroidLysis from the Git repository (preferred) or from pip 4. Configure `droidconfig.py` -### Step1: Install required system packages - -`sudo apt-get install default-jre git python3 python3-pip unzip wget libmagic-dev libxml2-dev libxslt-dev` - -### Step 2: Install Android disassembly tools - -DroidLysis does not perform the disassembly itself, but relies on other tools to do so. Therefore, you must install: - -- [Apktool](https://ibotpeaches.github.io/Apktool/) - note we only need the Jar. -- [Baksmali](https://bitbucket.org/JesusFreke/smali/downloads) - note we only need the Jar. +Install required system packages: -Optionally: - -- [Dex2jar](https://github.com/pxb1988/dex2jar) - dex2jar is now *optional*. If you don't need Dex to Jar transformation (useful for later decompiling!), you can skip it. -- [Procyon](https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler) - *optional*. If you don't want to use this decompiler, skip its installation. - -Some of these tools are redundant, but sometimes one fails on a sample while another does not. DroidLysis detects this and tries to switch to a tool that works for the sample. +``` +sudo apt-get install default-jre git python3 python3-pip unzip wget libmagic-dev libxml2-dev libxslt-dev +``` -For example, +Install Android disassembly tools: [Apktool](https://ibotpeaches.github.io/Apktool/) , +[Baksmali](https://bitbucket.org/JesusFreke/smali/downloads), and optionally +[Dex2jar](https://github.com/pxb1988/dex2jar) and +[Procyon](https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler) (note that Procyon only works with Java 8, not Java 11). ``` $ mkdir -p ~/softs $ cd ~/softs -$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.6.0.jar +$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.7.0.jar $ wget https://bitbucket.org/JesusFreke/smali/downloads/baksmali-2.5.2.jar -$ wget https://github.com/pxb1988/dex2jar/files/1867564/dex-tools-2.1-SNAPSHOT.zip -$ unzip dex-tools-2.1-SNAPSHOT.zip -$ rm -f dex-tools-2.1-SNAPSHOT.zip +$ wget https://github.com/pxb1988/dex2jar/releases/download/v2.2-SNAPSHOT-2021-10-31/dex-tools-2.2-SNAPSHOT-2021-10-31.zip +$ unzip dex-tools-2.2-SNAPSHOT-2021-10-31.zip +$ rm -f dex-tools-2.2-SNAPSHOT-2021-10-31.zip ``` -### Step 3a. Get DroidLysis from the Git Repository - -This is the most up-to-date version. - +Install from Git in a Python virtual environment: ``` -$ git clone https://github.com/cryptax/droidlysis -$ cd droidlysis -$ pip3 install -r requirements.txt +$ python3 -m venv venv +$ source ./venv/bin/activate +(venv) $ pip3 install git+https://github.com/cryptax/droidlysis ``` -### Step 3b. Get DroidLysis from Pypi - -Alternatively, you can install DroidLysis from pip3. Note the package may be slightly behind the git repository. +Run it: ``` -$ python3 -m venv droidlysis -$ cd droidlysis -$ source ./bin/activate -(droidlysis) /droidlysis # pip3 install droidlysis +cd droidlysis +./droidlysis --help ``` -### Step 4. Configure `droidconfig.py` +Alternatively, you can install DroidLysis directly from PyPi (`pip3 install droidlysis`). + +## Configuration -The configuration is extremely simple, you only need to tune `droidconfig.py`: +If you used the default install commands & directories as specified above, you won't need any configuration. + +The configuration is extremely simple, you only need to tune `droidconfig.py`. Note that if you placed the tools in the default `~/softs` directory as I specified, you don't have to do anything: the tools will be automatically found in that location. - `APKTOOL_JAR`: set the path to your apktool jar - `BAKSMALI_JAR`: set the path to your baksmali jar @@ -88,18 +75,19 @@ - `PROCYON_JAR`: set the path to the procyon decompiler jar. If you don't want Procyon, leave this path to a non existant file. - `INSTALL_DIR`: set the path to your DroidLysis instance. Do not forget to set this or DroidLysis won't work correctly! -Example: +By default, `droidconfig.py` searches for tools at the following location: ```python -APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.5.0.jar") +APKTOOL_JAR = os.path.join( os.path.expanduser("~/softs"), "apktool_2.7.0.jar") BAKSMALI_JAR = os.path.join(os.path.expanduser("~/softs"), "baksmali-2.5.2.jar") -DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.s -h") +DEX2JAR_CMD = os.path.join(os.path.expanduser("~/softs/dex-tools-2.1-SNAPSHOT"), "d2j-dex2jar.sh") PROCYON_JAR = os.path.join( os.path.expanduser("~/softs"), "procyon-decompiler-0.5.36.jar") INSTALL_DIR = os.path.expanduser("~/droidlysis") ``` -Optionally, if you need a specific situation, you might need to tune the following too. Normally, the default options will work and you won't have to touch these: +Optionally, if you need a specific situation, you might need to tune the following too. +Normally, the default options will work and you won't have to touch these: + - `SQLALCHEMY`: specify your SQL database. - `KEYTOOL`: absolute path of `keytool` which generally ships with Java - `SMALI_CONFIGFILE`: smali patterns @@ -113,34 +101,32 @@ DroidLysis uses **Python 3**. To launch it and get options: ``` -python3 ./droidlysis3.py --help +droidlysis --help ``` For example, test it on [Signal's APK](https://signal.org/android/apk/): ``` -python3 ./droidlysis3.py --input Signal-website-universal-release-4.52.4.apk --output /tmp +droidlysis --input Signal-website-universal-release-4.52.4.apk --output /tmp ``` -![](./example.png) +![](./images/example.png) DroidLysis outputs: -- A summary on the console (see example.png) +- A summary on the console (see image above) - The unzipped, pre-processed sample in a subdirectory of your output dir. The subdirectory is named using the sample's filename and sha256 sum. For example, if we analyze the Signal application and set `--output /tmp`, the analysis will be written to `/tmp/Signalwebsiteuniversalrelease4.52.4.apk-f3c7d5e38df23925dd0b2fe1f44bfa12bac935a6bc8fe3a485a4436d4487a290`. - A database (by default, SQLite `droidlysis.db`) containing properties it noticed. ## Options -Get usage with `python3 ./droidlysis3.py --help` +Get usage with `droidlysis --help` - The input can be a file or a directory of files to recursively look into. DroidLysis knows how to process Android packages, DEX, ODEX and ARM executables, ZIP, RAR. DroidLysis won't fail on other type of files (unless there is a bug...) but won't be able to understand the content. -- When processing directories of files, it is typically quite helpful to move processed samples to another location to know what has been processed. This is handled by option `--movein`. Also, if you are only interested in statistics, you should probably clear the output directory which contains detailed information for each sample: this is option `--clearoutput`. +- When processing directories of files, it is typically quite helpful to move processed samples to another location to know what has been processed. This is handled by option `--movein`. Also, if you are only interested in statistics, you should probably clear the output directory which contains detailed information for each sample: this is option `--clearoutput`. If you want to store all statistics in a SQL database, use `--enable-sql` (see [here](#sqlite_database)) -- When dealing with single samples, on the contrary, statistics are typically not so interesting, and their generation can be disabled with `--disable-sql` - -- DEX decompilation is quite long with Procyon, so this option is disabled by default. If you want to decompile to Java, use `--enable-procyon`. +- DEX decompilation is quite long with Procyon, so this option is *disabled* by default. If you want to decompile to Java, use `--enable-procyon`. - DroidLysis's analysis does not inspect known 3rd party SDK by default, i.e. for instance it won't report any suspicious activity from these. If you want them to be inspected, use option `--no-kit-exception`. This usually creates many more detected properties for the sample, as SDKs (e.g. advertisment) use lots of flagged APIs (get GPS location, get IMEI, get IMSI, HTTP POST...). @@ -163,7 +149,7 @@ If you do not need the sample output directory to be generated, use the option `--clearoutput`. -## SQLite database +## SQLite database{#sqlite_database} If you want to process a directory of samples, you'll probably like to store the properties DroidLysis found in a database, to easily parse and query the findings. In that case, use the option `--enable-sql`. This will automatically dump all results in a database named `droidlysis.db`, in a table named `samples`. Each entry in the table is relative to a given sample. Each column is properties DroidLysis tracks. @@ -191,20 +177,18 @@ description=Sending SMS messages ``` - ## To do -- The code is quite crappy now. I could probably do the same in less lines! -- Replace print by logging -- Remove the "caution: filename not matched: classes6.dex" which occurs at file extraction in droidsample.py +- Remove the "caution: filename not matched: classes6.dex" which occurs at file extraction in `droidsample.py` ## Updates -v3.4.0 - Multidex support -v3.3.1 - Improving detection of Base64 strings -v3.3.0 - Dumping data to JSON -v3.2.1 - IP address detection -v3.2.0 - Dex2jar is optional -v3.1.0 - Detection of Base64 strings +- v3.4.1 - Removed dependency to Androguard +- v3.4.0 - Multidex support +- v3.3.1 - Improving detection of Base64 strings +- v3.3.0 - Dumping data to JSON +- v3.2.1 - IP address detection +- v3.2.0 - Dex2jar is optional +- v3.1.0 - Detection of Base64 strings diff -Nru droidlysis-3.4.0/setup.py droidlysis-3.4.1/setup.py --- droidlysis-3.4.0/setup.py 2022-01-18 12:44:15.000000000 +0000 +++ droidlysis-3.4.1/setup.py 2023-02-21 13:06:39.000000000 +0000 @@ -6,8 +6,8 @@ long_description = fh.read() setup( - name = 'droidlysis', - description='DroidLysis: pre-analysis script for suspicious Android samples', + name='droidlysis', + description='DroidLysis: pre-analysis of suspicious Android samples', long_description=long_description, long_description_content_type="text/markdown", author='@cryptax', @@ -15,8 +15,8 @@ url='https://github.com/cryptax/droidlysis', license='MIT', keywords="android malware reverse", - python_requires='>=3.0.*', - version = '3.4.0', + python_requires='>=3.0', + version='3.4.1', packages=['conf'], py_modules=[ 'droidconfig', @@ -38,6 +38,11 @@ "Topic :: Software Development :: Disassemblers", ], include_package_data=True, - install_requires=[ 'python-magic', 'SQLAlchemy', 'rarfile', 'androguard' ], - scripts = [ 'droidlysis' ], + install_requires=[ + 'configparser>=4.0.2', + 'python-magic==0.4.12', + 'SQLAlchemy>=1.1.1', + 'rarfile>=3.0' + ], + scripts=[ 'droidlysis', 'droidlysis3.py' ] )