Normal view

There are new articles available, click to refresh the page.
Before yesterdayPentest/Red Team

How I Hacked an Android App to Get Free Beer

18 August 2016 at 06:37
How I Hacked an Android App to Get Free Beer

Just recently I stumbled upon an Android app that lets you receive free products in various pubs, restaurants or cafes in exchange for points accumulated with previous purchases. When the purchase is made, you let the vendor know that you want to receive points. In the app you select the types of products you bought. The eligible types of products may be "Beer", "Lunch" or "Spent 50 PLN". It all depends on the place. In order to verify the purchase, the vendor needs to swipe a physical beacon device over your phone (or enter a PIN if that doesn't work) and the application magically approves the transaction, granting you points.

As an example, one of the places offers you a free beer for 5 points and each purchased beer grants you 1 point. That gives you a free beer for every 5 purchased beers in that place.

Everyone likes free beer, so the first thing I thought about is how secure the purchase verification process is and how exactly do these magical beacons work?

More importantly, was there any way to get around the application's security and get a taste of free beer?

How I Hacked an Android App to Get Free Beer

I intentionally don't want to mention the name of the application as it only operates in my home country (in Poland). My goal is to give you an idea of what flaws similar applications may have, how to find them and how to better secure such applications. I've retained all technical details on how the application works, occasionaly replacing some easy to identify IDs or private information with random data.

In this post I will use a fictional name for the discussed application - "EatApp".

With that out of the way, let's get started!

Doing the research

The first thing I was most curious about was the beacon technology that is used with the application. The beacons apparently communicate with the mobile phone over bluetooth as the application made it clear that bluetooth needs to be turned on for the beacon swipes to work.

After a very short time I found the company that manufactures the same beacons that I saw working with EatApp. The company is Estimote and this is what they write about their beacon technology:

Estimote Beacons and Stickers are small wireless sensors that you can attach to any location or object. They broadcast tiny radio signals which your smartphone can receive and interpret, unlocking micro-location and contextual awareness.

With the Estimote SDK, apps on your smartphone are able to understand their proximity to nearby locations and objects, recognizing their type, ownership, approximate location, temperature and motion. Use this data to build a new generation of magical mobile apps that connect the real world to your smart device.

Estimote Beacons are certified Apple iBeacon™ compatible as well as support Eddystone™, an open beacon format from Google.

Apparently EatApp application detects the restaurant's beacon in close proximity, retrieves some identification values from the device and uses them to authorize the registration of new points with EatApp server.

How I Hacked an Android App to Get Free Beer

Thankfully Estimote has released an SDK with a very detailed documentation.

That allowed me to learn more about what information the beacon transmits. More technical information says that:

Estimote Beacon is a small computer. Its 32-bit ARM® Cortex M0 CPU is accompanied by accelerometer, temperature sensor, and what is most important—2.4 GHz radio using Bluetooth 4.0 Smart, also known as BLE or Bluetooth low energy.

I've also learned that beacons broadcast the following values:

  • UUID - most commonly represented as a string, e.g. “B9407F30-F5F8-466E-AFF9-25556B57FE6D”
  • Major number - an unsigned short integer, i.e., an integer ranging from 1 to 65535, (0 is a reserved value)
  • Minor number - also an unsigned short integer, like the major number.

Great! They also provide their own Android library to make it very easy for any application to listen to beacon broadcasts. Here is one of the example code snippets from the tutorial on how to set up the beacon listener:

beaconManager = new BeaconManager(getApplicationContext());
// add this below:
beaconManager.connect(new BeaconManager.ServiceReadyCallback() {
    @Override
    public void onServiceReady() {
        beaconManager.startMonitoring(new Region(
                "monitored region",
                UUID.fromString("B9407F30-F5F8-466E-AFF9-25556B57FE6D"),
                22504, 48827));
    }
});

It looks to me that the UUID number must be constant and unique to the application using it. Major and Minor numbers on the other hand can describe the product, so in EatApp scenario they must be unique for every restaurant.

Whenever the application waits for the vendor to swipe their beacon over the phone, it listens for packets with specific UUID. If the broadcast packet is detected, it uses signal strength value to measure the beacon's proximity to the Android device. If the beacon's signal strength indicates that the device is close enough, it uses the Major and Minor numbers from the packet as validation keys, which are sent with the authorization packet to the EatApp's server.

I wondered what was the maximum range over which, the beacon was able to transmit its packets. The application must be constantly listening for beacon broadcast messages as it even gives you a push notification when you enter the restaurant where EatApp beacon is present. I didn't have to search long for an answer:

Estimote Beacons have a range of up to 70 meters (230 feet). The signal, however, can be diffracted, interfered with, or absorbed by water (including the human body). That’s why in real world conditions you should expect range of about 40–50 meters.

Wow. Up to 70 meters? That means that in theory the security keys (UUID, Major and Minor numbers), that are very likely used for authorizing the rewards, are broadcasted in clear air! That can't be good.

Knowing that beacon broadcast packets can be received over such range, I needed to find a way how to receive the packets and read their contents. It would probably take me few days to write my own Android app using Estimote SDK, but thankfully Estimote provides their own Estimote Developer App for debugging and troubleshooting problems. From the screenshots I could tell that it gathers all the critical information.

At that moment, obtaining the beacon information would not do me much good without any insight on how the application communicates with the server. It was time to set up a small lab for intercepting and decrypting HTTPS communication from the mobile phone.

The Fiddler in the Middle

For intercepting mobile phone traffic, I've used a Windows box. The best and free HTTP/HTTPS Windows proxy for inspecting and forging new packets, that I know of, is Fiddler.

Setting up Fiddler

In order to enable HTTPS interception in Fiddler, open Tools > Telerik Fiddler Options > HTTPS and make sure Capture HTTPS CONNECTs and Decrypt HTTPS traffic is checked. Also make sure that under Tools > Telerik Fiddler Options > Connections, you ticked the Allow remote computers to connect option.

You will also need to export Fiddler's Certificate Authority certificate. In the same tab, click Actions and click Export Root Certificate to Desktop like so:

How I Hacked an Android App to Get Free Beer

This will put a file named FiddlerRoot.cer on your desktop. This is the root certificate that Fiddler will use to generate forged certificates for every HTTPS connection that goes through the proxy. Obviously Fiddler's generated root certificate won't be on your phone's list of trusted certificate authorities and any HTTPS connection that goes through the proxy will be blocked. That's why you need to import Fiddler's certificate on your phone and add it to trusted CA storage.

To do that, first copy the FiddlerRoot.cer to the SD card on your phone by any means. On your phone open Settings > Security and select Install from SD card:

How I Hacked an Android App to Get Free Beer

Find and pick Fiddler's certificate file in order to import it. Now your phone will trust Fiddler's proxy and you will be able to intercept and decrypt HTTPS traffic. You need to make sure that both your phone and your Windows box with Fiddler are running on the same network.

Before you proceed, find out which port Fiddler's proxy listens on, by opening Tools > Telerik Fiddler Options > Connections and checking the port box:

How I Hacked an Android App to Get Free Beer

Next, find out the local network IP address of your Windows box. Open up cmd.exe command line prompt and type in ipconfig. You should be able to find your current IP address under the section with the network interface that you are currently using.

On your Android phone, open up Settings > Wi-Fi and find the wireless network that you are connected to. Touch the network entry for 2 seconds and select Modify network from the drop-down menu. In the dialog tick Advanced options and scroll down to proxy settings. For the proxy type you need to set Manual and under hostname and proxy port enter the IP address of your Windows box and the Fiddler's proxy port. I entered 192.168.0.14 as the hostname and 9090 as the proxy port number.

Now if everything went fine, you should be able to see the outgoing mobile phone traffic in Fiddler.

Capturing traffic

With this setup I was able to intercept EatApp's traffic as the application didn't implement certificate pinning. Otherwise, it would require more work to be done. I'd have to decompile the application, remove the certificate comparison check and recompile the application.

I opened up EatApp and opened up Earn Points dialog for randomly picked restaurant. As I didn't have the restaurant's Estimote Beacon on me (duh!), I had to use the option to enter the PIN number to verify the point rewards. I entered the random PIN number and checked the intercepted packets in Fiddler.

Sent request:

POST https://api.eatapp.com/users/461845f5d03e6c052a43afbc/points HTTP/1.1
Accept: application/json
Accept-Language: en-us
X-App-Version: 1.28.0
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.4;)
...
Content-Type: application/json; charset=UTF-8
Content-Length: 265
Host: api.eatapp.com
Connection: Keep-Alive
Accept-Encoding: gzip

{
  "authentication_token":"boKUp9vBHNAJp7XbWZCK",
  "point":{
    "promoted_products_ids":[
      {"id":"760493597149625959620000"},
      {"id":"760493597149625959620000"}
    ],
    "pin":"1234",
    "place_id":"6088",
    "isDoneByGesture":false
  },
  "longitude":0.0,
  "latitude":0.0
}

Received reply:

HTTP/1.1 422 Unprocessable Entity
Server: nginx
Date: ...
Content-Type: application/json; charset=utf-8
Content-Length: 99
Connection: keep-alive
Vary: Accept-Encoding

{
  "status":"422 Unprocessable Entity",
  "code":"incorrectArgument",
  "title":"Incorrect argument: pin."
}

The request is not complicated. Parameters are sent as JSON data, there are no hash values being sent as a form of anti-tampering method with account state verification and the PIN is sent just in plain-text. The parameters are pretty self explanatory:

  • authentication_token - This is the account's authentication token that was received from the server during the login process. This value is unique to every EatApp account and won't change.
  • promoted_products_ids - the array of product type IDs that we are earning points for.
  • pin - the PIN number that we've entered.
  • place_id - the unique ID of the restaurant where we want to earn points.
  • isDoneByGesture - this one is a mystery, but I believe it is set to true only when you spend your points.
  • longitute and latitude - These are the last known GPS location values that were retrieved recently, letting the server know user's exact geographical location. This could be used as a security measure to detect if we are not too far away from the restaurant we are receiving points in.

At this point I wanted to try out if the application is vulnerable to PIN brute-forcing. After all, there are only 10'000 possible PIN combinations. Unfortunately after sending about 5 requests with different PIN values, I started to receive the following reply from the server:

HTTP/1.1 422 Unprocessable Entity
Server: nginx
Date: ...
Content-Type: application/json; charset=utf-8
Content-Length: 289
Connection: keep-alive
Vary: Accept-Encoding

{
  "status":"422 Unprocessable Entity",
  "code":"pinTooManyAttempts",
  "title":"Too many pin code attempts",
  "header":"Account locked",
  "message":"Earning and spending points and redeeming deals using your account has been locked for the next 30 minutes. Please let us know if this is a mistake."
}

Unless I had hundreds of EatApp accounts that I could switch between during the brute-force process, the 30 minutes account lockdown is a pretty strong deterrent.

Looking at the parameters of the verification request, I wondered what would the parameters be if the verification was done with the beacon swipe rather than with entering the PIN.

One idea of how free points could be earned, would be to remotely intercept the request packet with the correct PIN value entered by the restaurant's staff. Intercepting the packet remotely would also give me an opportunity to find the exact request parameters if beacon swipe was used instead of the PIN.

Main obstacle in performing this task is that I had to intercept the request from the mobile device, while I was at the restaurant.

That was a great opportunity to try setting up the interception VPN that would be used with 3G/4G connection on my mobile phone. The VPN server would then intercept and decrypt the HTTPS traffic the same way Fiddler did.

The Evil VPN

First of all, I required a VPS where I could install the VPN software. The fastest, easiest and most reliable way to set up a Linux box VPS is Digital Ocean (you will get 10$ credit if you sign up from this link). I created the cheapest Debian 8 1CPU 512MB RAM droplet for 5$/month or 0.007$/hour. This had to be more than enough for my needs.

I had to decide which VPN protocol I wanted to use. Android officially supports PPTP and L2TP types of VPN protocols. I've learned though that although PPTP is supported, it is considered insecure and Google doesn't trust it enough to enable Always-On feature for this kind of VPN. Always-On VPN feature in Android makes sure that your phone will reconnect to the VPN whenever the connection breaks and makes sure that no other packet will ever be sent NOT through the VPN. This is very important as I wanted to be absolutely sure that every single packet gets intercepted and decrypted.

Finding a good tutorial that would teach me how to install L2TP VPN on Debian 8 was very hard as most tutorials were written for Debian 7 and apparently in the later version of the system some dependencies changed and the tutorials became outdated.

Finally I found a perfect way to install IPsec/L2TP VPN with auto setup scripts. This is basically all I had to do on the server:

wget https://git.io/vpnsetup -O vpnsetup.sh
nano -w vpnsetup.sh
[Replace with your own values: YOUR_IPSEC_PSK, YOUR_USERNAME and YOUR_PASSWORD]
sudo sh vpnsetup.sh

YOUR_IPSEC_PSK - this should be you preshared key phrase (e.g. my_secret_psk_for_vpn).
YOUR_USERNAME and YOUR_PASSWORD - your username and password to login into the VPN.

All done in fire and forget manner! The VPN was up and running. Now it was time to set up the VPN connection on my phone. I went to Settings > Wireless & Networks (More) > VPN and added a new VPN and filled the settings as follows:

Name: vpn_interceptor
Type: L2TP/IPSec PSK
Server address: [VPN_SERVER_IP]
L2TP secret: (not used)
IPSec identifier: (not used)
IPSec pre-shared key: [YOUR_IPSEC_PSK]
DNS search domains: (not used)
DNS servers: 8.8.8.8
Forwarding routes: (not used)

Now that the VPN was added, I clicked the triple-dot Settings button in the same window and clicked Always-On option and then picked the newly created VPN connection.

This is when I ran into a problem on my Android 6.0. No matter how hard I wanted the VPN to connect, it would always show connection error. I tried on another Android 4.4 device and it worked perfectly, so I knew something was wrong with the latest version of Android. The Github page of the auto script did mention a workaround for Android 6 Marshmellow:

Note: Android 6 (Marshmallow) users should edit /etc/ipsec.conf on the VPN server and append ,aes256-sha2_256 to both ike= and phase2alg= lines. Then add a new line sha2-truncbug=yes immediately after those. Indent lines with two spaces. When finished, run service ipsec restart.

That didn't work, so I removed that "fix" from the server config files. Shortly after, I found a feature in the VPN profile settings called Backwards-compatible mode. I set that to Enable and *bang* - finally everything started to work.

Now that I had a working VPN, it was time to set up the interception and decryption of HTTPS packets. For that I decided to use SSLsplit software. Installation was easy:

wget http://mirror.roe.ch/rel/sslsplit/sslsplit-0.5.0.tar.bz2
tar jxvf sslsplit-0.5.0.tar.bz2
cd sslsplit-0.5.0
make
sudo make install

The IPSec/L2TP installation script created some advanced firewall settings for my new VPN setup and honestly I was not able to adjust it in such a way that allowed me to redirect HTTP/HTTPS packets to sslsplit proxy. I decided to completely purge the iptables settings and replace the iptables config file with the clean one, including some settings for redirecting packets to sslsplit.

iptables -F
iptables -t nat -F
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -t nat -A POSTROUTING -o ppp+ -j MASQUERADE
iptables-save > /etc/iptables.rules

Packet forwarding should already have been enabled (and should be re-enabled at every boot), because of the VPN auto setup script, but in order to be sure, you could run:

echo 1 > /proc/sys/net/ipv4/ip_forward

Next, I prepared the directory structure for sslsplit log files and created the sslsplit root certificate that will be used for generating forged HTTPS certificates the same way that Fiddler did before:

mkdir sslsplit
mkdir sslsplit/certs
mkdir sslsplit/logs
cd sslsplit/certs
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt

I had to download sslsplit/certs/ca.crt root certificate from the VPS, copy it my phone's SD card and import it as trusted certificate authority. That way my Android phone would allow any forged certificate generated by sslsplit to be accepted. You could easily download ca.crt file from your VPS via SSH protocol on Windows using WinSCP.

In order to easily turn on and off our HTTP/HTTPS interception, on the server, I decided to create two small shell scripts (my sslsplit directory was put in /root/):

/root/sslsplit/start.sh

#!/bin/bash
iptables-restore < /etc/iptables.rules
iptables -t nat -A PREROUTING -i ppp+ -p tcp --dport 80 -j REDIRECT --to-ports 8080
iptables -t nat -A PREROUTING -i ppp+ -p tcp --dport 443 -j REDIRECT --to-ports 8443
sslsplit -d -l /root/sslsplit/connections.log -j /root/sslsplit/ -F /root/sslsplit/logs/%T -k certs/ca.key -c certs/ca.crt ssl 0.0.0.0 8443 tcp 0.0.0.0 8080

/root/sslsplit/stop.sh

#!/bin/bash
killall sslsplit
iptables-restore < /etc/iptables.rules

Now whenever I wanted to start intercepting packets, I'd run ./start.sh and if I wanted to stop, ./stop.sh. Simple. The sslsplit was configured to run as a daemon and it would log all raw packets into separate files in /root/sslsplit/logs/ directory. The logging feature of sslsplit is not perfect as it doesn't fully decode saved HTTP packets, it doesn't group packets in request/reply order and will put several packets into one file if they are sent at the same exact second, but for my needs it had to do fine. I will later describe how I overcame the HTTP decoding issue.

With the VPN setup done and HTTPS interception in place, I was finally able to drive to town, eat something and do some live packet capturing!

Trip to town

I visited three places and ordered some food in each one of them. During checkout I asked the vendor to register some EatApp points for the products I just had. At that point my phone was all the time connected to the interception VPN and the authorization packets were logged on the server by sslsplit.

I found out that if the location permission was turned off, the beacon proximity feature wouldn't work, thus in two places the staff had to use the PIN authorization. I retrieved two packets with correct PIN values in the first two places and in the third place I was able to capture the beacon authorization packet, after I enabled the location permission for the app.

Finally I had an opportunity to check out the official Estimote developer's app that was supposed to detect nearby beacon broadcasts and retrieve the broadcasted UUID, Major and Minor. It turned out my theory was correct:

How I Hacked an Android App to Get Free Beer

I still had to confirm at home if the broadcasted authorization keys were really used in the authorization packet, by analyzing the sslsplit log files.

Connecting the dots

I downloaded the sslsplit log files from the server. The issue with sslsplit is that it will log all HTTP packets in their raw form. Meaning that if the Transfer-Encoding is chunked or packet is compressed with gzip, the packets won't be logged in decoded plain-text form.

I have released a small script that is supposed to decode the sslsplit packets into clear text form. Please note that script uses a quite buggy http-parser library that I decided to use for parsing HTTP packets. For simple needs, though, it is "good enough". You can find splitparse.py on Github here.

Usage:

pip install http-parser
python splitparse.py -i [sslsplit_logs_dir] -o [output_dir]

I quickly found the request packet with the authorization data that was sent when the beacon was swiped over my phone:

POST /users/461845f5d03e6c052a43afbc/points
Accept: application/json
Accept-Language: en-gb
X-App-Version: 1.28.0
User-Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1;)
...
Content-Type: application/json; charset=UTF-8
Content-Length: 375
Host: api.eatapp.com
Connection: Keep-Alive
Accept-Encoding: gzip

{
  "authentication_token":"boKUp9vBHNAJp7XbWZCK",
  "latitude":...,
  "longitude":...,
  "point":{
    "isDoneByGesture":false,
    "main_beacon":{
      "major":38995,
      "minor":12702,
      "uuid":"2C75E74B-41B7-49E3-BD26-CE86B2F569F8"
    },
    "place_id":"450",
    "promoted_products_ids":[
      {"id":"647035946536601578040000"},
      {"id":"647035946536601578040000"},
      {"id":"647035946536601578050000"}
    ]
  }
}

Jackpot! I confirmed that the UUID, Major and Minor numbers were exactly the same in the request authorization packet and detected live with the Estimote developer's app. That meant my theory was correct and the verification keys are constantly being broadcasted over the air in every EatApp supported restaurant.

To summarize, here is the step-by-step guide on how to get free EatApp points in restaurant ZZZ:

  • Walk into restaurant ZZZ.
  • Open up Estimote developer's app and detect the closest nearby beacon.
  • Save the screenshot with the visible UUID, Major and Minor values.
  • Go home.
  • Set a breakpoint in Fiddler to intercept EatApp packets with /users/ path in GET requests.
  • On your phone, select the ZZZ restaurant and set EatApp to await PIN authorization for earned points.
  • Enter any PIN.
  • Modify the intercepted packet in Fiddler, removing the "pin":"NNNN" entry and replacing it with the valid "main_beacon":{...} content containing the beacon keys captured with Estimote app.
  • Let the modified packet through to EatApp server.
  • Enjoy your free points!

Of course it would be much better to write you own tool to directly communicate with application's server API while implementing proper location spoofing. The method I described is just quicker and easier for testing purposes.

Conclusion

Broadcasting authorization keys publicly over the air is never a good idea. Here is the list of things that could be done to improve EatApp's security:

  • Send a hash value of account's current state as an additional request parameter (points for every restaurant, account name, last sent GPS location etc.). The server would then verify if the hash is correct and only then authorize the request. Finding out how the hash parameter is formed would not be possible without disassembling and reverse engineering the application's code. That would add additional difficulty.
  • In order to make reverse engineering harder, the application's code should be obfuscated. If the additional hash verification parameter was implemented, it would greatly increase the difficulty of reverse engineering the application's code.
  • @FabricatorGeneral mentioned in the comment section that it is possible to enable Secure UUID feature in Estimote's beacons that would broadcast the beacon keys in encrypted form. That way only applications with the correct API key would be able to decrypt it. Bypassing that would require writing a custom mobile application implementing Estimote SDK, that would listen to broadcasts, decrypting them with application's API key, that would have to be retrieved first by reverse engineering the application's code.
  • Certificate pinning should be implemented to make it harder to intercept the HTTPS connection using forged certificates. This security feature could also be bypassed, but it would involve reverse engineering the application, finding where the certificate verification check is done, removing it and recompiling the application again.
  • Not sure if that is possible with Estimote Beacon's, but it would be good to provide restaurants with a second beacon that would be used only for authorizing transactions with maximum broadcast range of half meter.
  • Never trust the client's device! For maximum security, client's device should only send the reward request to the server, without any authorization keys. Afterwards, the server would send the authorization request to vendor's dedicated tablet behind the counter, requesting him to authorize the point rewards with proper keys. Beacon swiping or entering the PIN would only be done on vendor's device, thus making it impossible for the client to intercept the request that he could later use to create forged packets.

I strongly hope that you've learned something new today and that you found this post entertaining.

If you have any questions, want to send your feedback or you want to contact me for any other reason, you can find me on Twitter @mrgretzky or contact me directly via e-mail at [email protected].

Till next time!

Now it's time to enjoy the spoils of hard work. Cheers!

Edit @2016-08-19: Added two new methods of securing the application in the Conclusion section.

How I Hacked an Android App to Get Free Beer

Android Basics

By: 0xe7
16 August 2015 at 17:06

Hacking Android is a huge topic, you have the Linux kernel, native code applications and normal Android applications that run on top of the Dalvik VM, a huge attack surface with the wireless, bluetooth, USB, NFC and various other interfaces.

This post is going to be a very short introduction to the platform as well as introducing some very basic analysis techniques for analyzing an Android application.

For this post I will be using a HTC Desire HD running an unrooted Android 2.3.5, this is an old version of Android, running on an old device but it will be fine for the purposes of this post.

The host machine that I'll be using is running 64bit Gentoo running the Linux kernel version 4.0.5.

Setting Up The Environment

The first thing you need to do if you want to analyze an Android device is to install the SDK.

Depending on your system, you might need to install both the Android Studio as well as the platform tools, its important that you have the platform-tools directory because that is the directory that contains the adb binary.

adb is the Android Debug Bridge and it's used to do any sort of debugging of any android device. Without this application, debugging Android will be very difficult.

On my Windows computer this was installed to C:\Users\User\AppData\Local\Android\sdk\platform-tools.

On my Debian-based system it was installed to $HOME/Android/Sdk/platform-tools and on my Gentoo system it was installed to /usr/bin.

Where ever it is installed to, it's best to include this directory in your PATH variable so that you can run it with cd'ing to that directory or having to put the whole file path in every time.

This is optional but on Linux I've been unable to get adb to work with running it as root, instead of having to use sudo all the time I set the permissions to the adb binary to 4750 and the ownership to root:wheel, this makes it a setuid binary and so will run with root permissions but only for users in the wheel group.

The permissions look as follows:

1
2
user@exploit ~ $ ls -l `which adb`
-rwsr-x--- 1 root wheel 156128 Aug  9 01:13 /usr/bin/adb

Exploring The System Using ADB

Firstly the Android device needs to be connected to the host machine using USB.

After that we need to enable USB debugging, on my test device the setting is in Settings->Applications->Development:

You will need to confirm this:

We can now use adb to get a shell on the Android device and take a look around, first let's check the version of the Linux kernel that it's running:

1
2
3
4
5
6
7
8
9
user@exploit ~ $ adb shell
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
$ pwd
/
$ uname -a
uname: permission denied
$ cat /proc/version
Linux version 2.6.35.10-g931a37e (htc-kernel@and18-2) (gcc version 4.4.0 (GCC) ) #1 PREEMPT Wed Nov 9 14:04:03 CST 2011

So as you can see it is, in fact, running Linux version 2.6.35 but in some sort of restricted environment.

Let's check to see some details of the user that we have been logged in as:

1
2
3
4
5
6
$ whoami
whoami: permission denied
$ w
w: permission denied
$ cat /etc/passwd
/etc/passwd: No such file or directory

As you can see due to the restricted environment, the normal methods aren't working.

But there is an easy way to figure this out:

1
2
$ ps | grep ' ps'
grep: permission denied

So that didn't work either, but there is a way to use grep on the output of these commands:

1
2
3
$ exit
user@exploit ~ $ adb shell ps | grep ' ps'
shell     5968  5967  944    332   00000000 afd0b83c R ps

Here we can see we're running at the user shell. Also note that we can run the commands using adb but then pipe the output to applications running on our host system to sort through the data.

Knowing our username we can now see what shell we are running in:

1
2
3
4
user@exploit ~ $ adb shell ps | grep shell
shell     6062  29118 788    336   c009ccc8 afd0c78c S /system/bin/sh
shell     6063  6062  944    332   00000000 afd0b83c R ps
shell     29118 1     3464   216   ffffffff 00000000 S /sbin/adbd

So this is clearly a Linux system but its very different from most Linux systems, let's have a look at the directories under /:

 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
28
user@exploit ~ $ adb shell ls -l /
drwxr-xr-x root     system            2015-07-28 18:35 app-cache
dr-x------ root     root              2015-07-28 18:33 config
lrwxrwxrwx root     root              2015-07-28 18:33 sdcard -> /mnt/sdcard
drwxr-xr-x root     root              2015-07-28 18:33 acct
drwxrwxr-x root     system            2015-07-28 18:33 mnt
lrwxrwxrwx root     root              2015-07-28 18:33 vendor -> /system/vendor
lrwxrwxrwx root     root              2015-07-28 18:33 d -> /sys/kernel/debug
lrwxrwxrwx root     root              2015-07-28 18:33 etc -> /system/etc
drwxrwx--- system   cache             2015-08-15 19:00 cache
drwx------ root     root              2015-08-16 09:53 devlog
-rw-r--r-- root     root          728 1970-01-01 01:00 ueventd.spade.rc
-rw-r--r-- root     root         4429 1970-01-01 01:00 ueventd.rc
-rw-r--r-- root     root            0 1970-01-01 01:00 ueventd.goldfish.rc
drwxr-xr-x root     root              2012-11-18 05:06 system
drwxr-xr-x root     root              2015-07-28 18:32 sys
drwxr-x--- root     root              1970-01-01 01:00 sbin
dr-xr-xr-x root     root              1970-01-01 01:00 proc
-rwxr-x--- root     root         7423 1970-01-01 01:00 init.spade.rc
-rwxr-x--- root     root        21309 1970-01-01 01:00 init.rc
-rwxr-x--- root     root         1677 1970-01-01 01:00 init.goldfish.rc
-rwxr-x--- root     root       107216 1970-01-01 01:00 init
-rw-r--r-- root     root          118 1970-01-01 01:00 default.prop
drwxrwx--x system   system            2015-02-16 07:05 data
-rw-r--r-- root     root         1401 1970-01-01 01:00 cwkeys
-rw-r--r-- root     root          460 1970-01-01 01:00 bootcomplete.rc
drwx------ root     root              2011-11-09 05:59 root
drwxr-xr-x root     root              2015-08-10 08:39 dev

Again, some of these are familiar (like etc, proc, dev...) but directories like system, acct and app-cache are less familiar.

I'm not going to go through the whole of Android, the point of this section was to demonstrate that this is a Linux system but not 1 that will seem totally familiar with Linux admins.

There are, however, a couple of commands I want to mention here, firstly getprop:

1
2
3
4
5
user@exploit ~ $ adb shell getprop | grep build.version
[ro.build.version.incremental]: [208029.5]
[ro.build.version.sdk]: [10]
[ro.build.version.codename]: [REL]
[ro.build.version.release]: [2.3.5]

Here I'm just using getprop to show some information about the build version of Android but you can get a lot of information from this.

The other 1 is logcat, which is basically the system log:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
user@exploit ~ $ adb logcat -t 8
--------- beginning of /dev/log/system
--------- beginning of /dev/log/main
D/dalvikvm(11665): GC_CONCURRENT freed 513K, 37% free 5018K/7879K, external 0K/0K, paused 3ms+2ms
D/dalvikvm(14049): GC_EXPLICIT freed 13K, 41% free 4222K/7047K, external 0K/0K, paused 102ms
D/dalvikvm(11665): GC_CONCURRENT freed 545K, 36% free 5193K/8071K, external 0K/0K, paused 2ms+2ms
D/dalvikvm(11665): GC_CONCURRENT freed 594K, 36% free 5338K/8263K, external 0K/0K, paused 9ms+2ms
D/dalvikvm(11665): GC_CONCURRENT freed 779K, 38% free 5355K/8519K, external 0K/0K, paused 3ms+11ms
D/dalvikvm(11665): GC_CONCURRENT freed 831K, 37% free 5428K/8583K, external 0K/0K, paused 2ms+2ms
D/dalvikvm(11665): GC_CONCURRENT freed 857K, 38% free 5414K/8647K, external 0K/0K, paused 2ms+3ms
D/dalvikvm(11665): GC_CONCURRENT freed 721K, 37% free 5460K/8647K, external 0K/0K, paused 4ms+6ms

Here I'm just outputing the last 8 lines of the log, as you can see it's an actual built in command into adb, but you can run logcat from inside the shell too.

logcat is very useful for debugging and well as showing some information disclosure vulnerabilities that might exist in an Android application, but we'll get to that a bit later :-)

Installing And Running The Challenge App

For the purposes of this post I created a little, very basic, challenge application, which you can download from here.

You can install the application using adb:

1
2
3
4
5
user@exploit ~ $ adb push Android/src/challenge1.apk /data/local/tmp/
2927 KB/s (1052648 bytes in 0.351s)
user@exploit ~ $ adb shell pm install /data/local/tmp/challenge1.apk
        pkg: /data/local/tmp/challenge1.apk
Success

Now that the application is installed we can run it and have a look at what it does.

Starting the application shows us this:

Putting in some text in to the field and clicking on the Check Password button shows us this:

So its clear what we have to try and do here.

Android Applications

Android applications come in apk format, these are just Java archive file:

1
2
user@exploit ~/Android/src $ file challenge1.apk
challenge1.apk: Java archive data (JAR)

These are very similar to zip files and can be unzipped using the unzip utility:

 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
28
29
30
user@exploit ~/Android/src $ mkdir challenge1
user@exploit ~/Android/src $ cd challenge1
user@exploit ~/Android/src/challenge1 $ unzip ../challenge1.apk
Archive:  ../challenge1.apk
  inflating: AndroidManifest.xml
  inflating: res/anim/abc_fade_in.xml
  inflating: res/anim/abc_fade_out.xml
  inflating: res/anim/abc_grow_fade_in_from_bottom.xml
  inflating: res/anim/abc_popup_enter.xml
  inflating: res/anim/abc_popup_exit.xml
  inflating: res/anim/abc_shrink_fade_out_from_bottom.xml
...
  inflating: res/layout/notification_template_lines.xml
  inflating: res/layout/notification_template_media.xml
  inflating: res/layout/notification_template_part_chronometer.xml
  inflating: res/layout/notification_template_part_time.xml
  inflating: res/layout/select_dialog_item_material.xml
  inflating: res/layout/select_dialog_multichoice_material.xml
  inflating: res/layout/select_dialog_singlechoice_material.xml
  inflating: res/layout/support_simple_spinner_dropdown_item.xml
  inflating: res/menu/menu_main.xml
 extracting: res/mipmap-hdpi-v4/ic_launcher.png
 extracting: res/mipmap-mdpi-v4/ic_launcher.png
 extracting: res/mipmap-xhdpi-v4/ic_launcher.png
 extracting: res/mipmap-xxhdpi-v4/ic_launcher.png
 extracting: resources.arsc
  inflating: classes.dex
  inflating: META-INF/MANIFEST.MF
  inflating: META-INF/CERT.SF
  inflating: META-INF/CERT.RSA

As you will see there are a lot of files in res/, I've shortened it here.

One of the most important files is AndroidManifest.xml, this contains the configuration of the application, including activity, intent and permissions declarations.

But as you can see this is some sort of binary file:

 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
user@exploit ~/Android/src/challenge1 $ file AndroidManifest.xml
AndroidManifest.xml: data
user@exploit ~/Android/src/challenge1 $ cat AndroidManifest.xml
„P4Rvœª¸.2Dx¬À,J^xŒ.fz
                         versionCode
minSdkVersiontargetSdkVersion       versionName
                             allowBackupiconlabelthemenameandroid*http://schemas.android.com/apk/res/androidpackageplatformBuildVersionCodeplatformBuildVersionNammanifest+ph.exploit.crack.cha5.1.1-181972uses-sdkge11.022
intent-filteractionandroid.intent.action.MAIcategory android.intent.category.LAUNCHER,nActivity
                                                                                      €ÿÿÿÿ
ˆÿÿÿÿÿÿÿÿ
ÿÿÿÿ
ÿÿÿÿ
ÿÿÿLÿÿÿÿÿÿÿÿ
ÿÿÿÿ

ÿÿÿÿ    ÿÿÿÿÿÿÿÿt
                 ÿÿÿÿÿÿÿÿ
ÿÿÿw
ÿÿÿÿ
ÿÿÿÿ
ÿÿÿÿÿÿÿLÿÿÿÿÿÿÿÿ
ÿÿÿÿ
$ÿÿÿÿÿÿÿÿ8ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ8ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ

It's actually binary XML.

We can manually convert this to plain XML using something like AXMLPrinter2 but I'll show an easier way to look at this later.

Another important file is the classes.dex file. This contains all of our Java code.

Most code that runs on Android applications, including most of the Android Framework, is Java, but instead of compiling to Java bytecode, it is compiled into Dalvik bytecode (and stored in .dex or .odex files).

Again, this could be manually decompiled back to Java using dex2jar and looked at using jd-gui but I'll show a better way of doing this too.

Doing A Very Basic Static Analysis

To end this post I'll crack this challenge while demonstrating a quick basic analysis of the apk file using a tool called androguard created by Anthony Desnos.

Androguard is a python toolset for analyzing apk files.

Its very easy to setup, providing you have python and git installed you just run:

1
git clone https://github.com/androguard/androguard.git

This will create a directory in the current directory called androguard.

Now you just need to make sure you have the latest version of IPython installed, you can do that by running:

1
pip install ipython

Now just cd to the androguard directory and you should see the following (or something very similar):

1
2
3
user@exploit ~/tools/androguard $ ls
androarsc.py  androaxml.py   androdd.py    androdis.py   androguard   androlyze.py  androsim.py   CHANGELOG  elsim     LICENCE-2.0  README.md  tests
androauto.py  androcsign.py  androdiff.py  androgexf.py  androgui.py  androsign.py  apkviewer.py  demos      examples  Makefile     setup.py   tools

The tool we'll be using is androlyze.py, we can start it by running:

1
2
3
user@exploit ~/tools/androguard $ ./androlyze.py -s
Androlyze version 3.0
In [1]:

Now we need to import our apk file:

1
2
3
In [1]: a,d,dx = AnalyzeAPK('/home/user/Android/src/challenge1.apk', decompiler='dad')

In [2]:

Here we are using the dad decompiler, created by Geoffroy Gueguen, that comes with androguard and selecting the challenge apk file.

Now we can look at this application in more detail:

 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
28
29
In [2]: a.get_activities()
Out[2]: ['ph.exploit.crack.challenge1.crackchallenge1.MainActivity']

In [3]: a.get_main_activity()
Out[3]: u'ph.exploit.crack.challenge1.crackchallenge1.MainActivity'

In [4]: a.get_permissions()
Out[4]: []

In [5]: a.get_receivers()
Out[5]: []

In [6]: print a.xml['AndroidManifest.xml'].toxml()
<?xml version="1.0" ?><manifest android:versionCode="1" android:versionName="1.0" package="ph.exploit.crack.challenge1.crackchallenge1" platformBuildVersionCode="22" platformBuildVersionName="5.1.1-1819727" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="10" android:targetSdkVersion="22">
</uses-sdk>
<application android:allowBackup="true" android:icon="@7F030000" android:label="@7F060012" android:theme="@7F080077">
<activity android:label="@7F060012" android:name="ph.exploit.crack.challenge1.crackchallenge1.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN">
</action>
<category android:name="android.intent.category.LAUNCHER">
</category>
</intent-filter>
</activity>
</application>
</manifest>

In [7]:

As you can see this is a very basic application, but we can now see the contents of the AndroidManifest.xml file.

An activity is basically just a screen, or, to relate it to a web application, a page. The main activity is the first activity that is shown to the user.

Let's try to look at some of the code.

First we'll print the different methods which are part of the main activity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
In [7]: for m in d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.get_methods():
   ...:     print m.get_name()
   ...:     
<init>
onClick
onCreate
onCreateOptionsMenu
onOptionsItemSelected

In [8]:

The first place to look here is the onCreate method.

This is the code that gets run when the application first starts:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
In [8]: d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.METHOD_onCreate.source()
protected void onCreate(android.os.Bundle p15)
    {
        super.onCreate(p15);
        android.widget.RelativeLayout v1_1 = new android.widget.RelativeLayout(this);
        v1_1.setBackgroundColor(-16777216);
        android.widget.RelativeLayout$LayoutParams v0_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v2_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v4_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v8_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v7_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        v0_1.addRule(14);
        v0_1.addRule(15);
        this.checkButton = new android.widget.Button(this);
        this.checkButton.setText("Check Password");
        this.checkButton.setBackgroundColor(-1);
        this.checkButton.setTextColor(-16777216);
        this.checkButton.setId(1);
        this.checkButton.setClickable(1);
        this.checkButton.setOnClickListener(this);
        this.inputBox = new android.widget.EditText(this);
        this.inputBox.setBackgroundColor(-1);
        this.inputBox.setTextColor(-16711936);
        this.inputBox.setId(2);
        v2_1.addRule(2, this.checkButton.getId());
        v2_1.addRule(14);
        v2_1.setMargins(0, 0, 0, 50);
        android.widget.TextView v3_1 = new android.widget.TextView(this);
        v3_1.setText("Please enter the secret password:");
        v3_1.setTextColor(-1);
        v3_1.setId(3);
        v4_1.addRule(2, this.inputBox.getId());
        v4_1.addRule(14);
        v4_1.setMargins(0, 0, 0, 50);
        android.widget.TextView v9_1 = new android.widget.TextView(this);
        v9_1.setText("Welcome to Challenge 1!");
        v9_1.setTextColor(-1);
        v9_1.setId(4);
        v8_1.addRule(2, v3_1.getId());
        v8_1.addRule(14);
        v8_1.setMargins(0, 0, 0, 50);
        this.resultText = new android.widget.TextView(this);
        this.resultText.setId(5);
        this.resultText.setVisibility(4);
        v7_1.addRule(3, this.checkButton.getId());
        v7_1.addRule(14);
        v7_1.setMargins(0, 50, 0, 0);
        this.inputBox.setWidth(((int) android.util.TypedValue.applyDimension(1, 1133903872, this.getResources().getDisplayMetrics())));
        v1_1.addView(this.checkButton, v0_1);
        v1_1.addView(this.inputBox, v2_1);
        v1_1.addView(v3_1, v4_1);
        v1_1.addView(v9_1, v8_1);
        v1_1.addView(this.resultText, v7_1);
        this.setContentView(v1_1);
        return;
    }


In [9]:

We can see here that the actual interface for this application is created dynamically using Java.

As you can see from line 20, this main class is registered as the onClickListener for the button that checks the value of the password.

This means that when you press the Check Password button, it will run the onClick method in this class.

So let's look at the code for that method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
In [9]: d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.METHOD_onClick.source()
public void onClick(android.view.View p4)
    {
        if (p4 == this.checkButton) {
            if (this.inputBox.getText().toString().equals("supersecurepassword") != 1) {
                this.resultText.setText("Incorrect password! :\'-(");
                this.resultText.setTextColor(-65536);
                this.resultText.setVisibility(0);
            } else {
                this.resultText.setText("Well done!! Challenge passed! :-)");
                this.resultText.setTextColor(-16711936);
                this.resultText.setVisibility(0);
            }
        }
        return;
    }


In [10]:

So here it's obvious that the password is supersecurepassword.

We can check that on the application itself:

It was fine this time but there are times when a decompilation will not be enough, the decompiler will not be able to recreate the source code well enough to get the right result.

In these cases you can use the disassembler like this:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
 In [10]: d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.METHOD_onClick.show()
########## Method Information
Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->onClick(Landroid/view/View;)V [access_flags=public]
########## Params
- local registers: v0...v3
- v4: android.view.View
- return: void
####################
***************************************************************************
onClick-BB@0x0 :
        0  (00000000) const/4             v2, 0
        1  (00000002) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->checkButton Landroid/widget/Button;
        2  (00000006) if-ne               v4, v0, 41 [ onClick-BB@0xa onClick-BB@0x58 ]

onClick-BB@0xa :
        3  (0000000a) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->inputBox Landroid/widget/EditText;
        4  (0000000e) invoke-virtual      v0, Landroid/widget/EditText;->getText()Landroid/text/Editable;
        5  (00000014) move-result-object  v0
        6  (00000016) invoke-virtual      v0, Ljava/lang/Object;->toString()Ljava/lang/String;
        7  (0000001c) move-result-object  v0
        8  (0000001e) const-string        v1, 'supersecurepassword'
        9  (00000022) invoke-virtual      v0, v1, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
        10 (00000028) move-result         v0
        11 (0000002a) const/4             v1, 1
        12 (0000002c) if-ne               v0, v1, 23 [ onClick-BB@0x30 onClick-BB@0x5a ]

onClick-BB@0x30 :
        13 (00000030) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        14 (00000034) const-string        v1, 'Well done!! Challenge passed! :-)'
        15 (00000038) invoke-virtual      v0, v1, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
        16 (0000003e) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        17 (00000042) const               v1, -16711936
        18 (00000048) invoke-virtual      v0, v1, Landroid/widget/TextView;->setTextColor(I)V
        19 (0000004e) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        20 (00000052) invoke-virtual      v0, v2, Landroid/widget/TextView;->setVisibility(I)V [ onClick-BB@0x58 ]

onClick-BB@0x58 :
        21 (00000058) return-void

onClick-BB@0x5a :
        22 (0000005a) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        23 (0000005e) const-string        v1, "Incorrect password! :'-("
        24 (00000062) invoke-virtual      v0, v1, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
        25 (00000068) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        26 (0000006c) const/high16        v1, -1
        27 (00000070) invoke-virtual      v0, v1, Landroid/widget/TextView;->setTextColor(I)V
        28 (00000076) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        29 (0000007a) invoke-virtual      v0, v2, Landroid/widget/TextView;->setVisibility(I)V
        30 (00000080) goto                -20 [ onClick-BB@0x58 ]

***************************************************************************
########## XREF
####################

In [11]:

Here we can see the actual dalvik disassembly and decompile it ourself if need be.

Conclusion

So we've learnt a bit about Android and how we can begin to analyze the system more.

We have been introduced to the basic layout of Android and Android applications, as well as a few tools that can be used to look a little closer at them.

Hopefully this blog post has done a decent job of introducing the basics of Android and given ideas for further exploration.

Further Reading

Android Hacker's Handbook by Joshua J. Drake, Zach Lanier, Collin Mulliner, Pau Oliva Fora, Stephen A. Ridley, Georg Wicherski

Easter Egg in APK Files: What Is Frosting

By: BI.ZONE
28 December 2020 at 14:04

By Konstantin Molodyakov

A file structure is a whole fascinating world with its own history, mysteries and a home-grown circus of freaks, where workarounds are applied liberally. If you dig deeper into it, you can discover loads of interesting stuff.

In our digging we came across a particular feature of APK files — a special signature with a specific block of metadata, i.e. frosting. It allows you to determine unambiguously if a file was distributed via Google Play. This signature would be useful to antivirus vendors and sandboxes when analyzing malware. It can also help forensic investigators pinpoint the source of a file.

There’s hardly any information out there regarding this topic. The only reference appears to be in Security Metadata in Early 2018 on the Android Developers Blog, and there is also an Avast utility that allows this signature to be validated. I decided to explore the feature and check the Avast developers’ assumptions about the contents of the frosting block and share my findings.

Frosting and APK Signing Block

Google uses a special signature for APK files when publishing apps on Google Play. This signature is stored in the APK Signing Block, which precedes the central directory of ZIP files and follows its primary contents:

The magic APK Sig Block 42 can be used to identify the APK Signing Block. The signing block may contain other blocks, whose application can be determined by the 4-byte ID. Thus, we get a ZIP format extension with backward compatibility. If you are interested in reading more or seeing the source code, you can check out the description of the method ApkSigningBlockUtils.findSignature here.

Let us take some file as an example, for instance 2124948e2b7897cd6fbbe5fbd655c26d. You can use androguard to view the block identifiers within the APK Signing Block:

There are several types of blocks with various identifiers, which are officially described in the documentation:

Some of the blocks can be found in Android source codes:

Other types of blocks that may come up:

  • 0x504b4453 (DEPENDENCY_INFO_BLOCK_ID) — a block that apparently contains dependency metadata, which is saved by the Android Gradle plugin to identify any issues related to dependencies
  • 0x71777777 (APK_CHANNEL_BLOCK_ID) — a Walle (Chinese gizmo) assembler block, which contains JSON with a channel ID
  • 0xff3b5998 — a zero block, which I ran into in the file — I couldn't find any information on that
  • 0x2146444e — a block with the necessary metadata from Google Play

Frosting and Play Market

Let us get back to analyzing the 0x2146444e block. First off, we should explore the innards of the Play Market application.

The identifier of our interest is found in two locations. As we delve deeper, we quite quickly spot the class responsible for block parsing. This is the first time that the name of a frosting block pops up among the constants:

Having compared different versions of the Play Market application, I have made the following observation: the code responsible for the parsing of this type of signature appeared around January 2018 together with the release of the 8.6.X version. While the frosting metadata block already existed, it was during this period that it took on its current form.

In order to parse the data, we need a primitive for reading 4-byte numbers. The scheme is a standard varint without any tricks involving negative numbers.

Though simple, the block parsing function is fairly large. It allows you to gain an understanding of the data structure:

To validate the signature, the first-field hash and key from validation_sequence are used, where validation_strategy equals zero. The signature itself is taken from signature_sequence with the same ordinal number as the validation_sequence entry. The figure below presents the explanatory pseudocode:

The signing_key_index value indicates the index in the array finsky.peer_app_sharing_api.frosting_public_keys, which contains only one key so far as shown below:

The size_signed_data is signed with the ECDSA_SHA256 algorithm starting with the size_frosting variable. Note that the signed data contains SHA-256 of the file data:

1) data from the beginning of the file to the signing block

2) data from the central directory to the end of the central directory, with the field value ‘offset of start of central with respect to the starting disk number’ at the end of the central directory replaced with a signing block offset

The signature scheme version 2 block (if any) is inserted between the data from the above items 1 and 2 with APK_SIGNATURE_SCHEME_V2_BLOCK_ID preceding it.

The hash calculation function in the Play Market application is represented as follows:

Frosting and ProtoBuf

This information is sufficient for signature validation. Alas, I failed to figure out what is hidden in the frosting block data. The only thing I was able to discover was the data has a ProtoBuf format and varies greatly in size and the number of fields depending on the file.

Typical representation of decoded data without a scheme (4b005c9e9ea0731330a757fcf3abeb6e):

But you can come across some instances (471c589acc800135eb318057c43a8068) with around five hundred fields.

The data occasionally contains such curious strings as: android.hardware.ram.low, com.samsung.feature.SAMSUNG_EXPERIENCE, com.google.android.apps.photos.PIXEL_2018_PRELOAD. These strings are not explicitly declared feature names, which a device may have.

You can find the description of available features in the files on the device — in the /etc/sysconfig/ folder:

If we were to give an example of a declared feature, this could be checking the camera availability by calling the android.hardware.camera function through the method PackageManager hasSystemFeature. However, the function of these strings within this context is vague.

I could not guess, find or recover the data scheme for the Play Market APK classes. It would be great if anyone could tell us what is out there and how they managed to figure it out. Meanwhile, all we have now are the assumptions of the Avast utility developers about the ProtoBuf structure and the string com.google.android.apps.photos.PIXEL_2018_PRELOAD indicating a system or pre-installed app:

I would like to share some of my comments with respect to the above.

1. When it comes to the string com.google.android.apps.photos.PIXEL_2018_PRELOAD: you can easily prove that this assumption is incorrect. If we download a few Google factory images, we will realize that they have neither such strings nor a single app with a frosting block.

We can look into it in more detail using the image walleye for Pixel 2 9.0.0 (PQ3A.190801.002, Aug 2019). Having installed the image, we are not able to spot a single file with a frosting block among a total of 187 APK files. If we update all the apps, 33 out of the 264 APK files will acquire a frosting block. However, only 5 of them will contain these strings:

com.google.android.as:

  • com.google.android.feature.DPS
  • com.google.android.feature.PIXEL_EXPERIENCE
  • com.google.android.feature.PIXEL_2017_EXPERIENCE
  • com.google.android.feature.PIXEL_2019_EXPERIENCE
  • com.google.android.feature.ANDROID_ONE_EXPERIENCE
  • com.google.android.feature.PIXEL_2018_EXPERIENCE
  • com.google.android.feature.PIXEL_2020_EXPERIENCE

google.android.inputmethod.latin:

  • android.hardware.ram.low

google.android.dialer:

  • com.google.android.apps.dialer.GO_EXPERIENCE
  • com.google.android.feature.PIXEL_2020_EXPERIENCE

google.android.GoogleCamera:

  • android.hardware.camera.level.full

google.android.apps.photos:

  • com.google.android.feature.PIXEL_2020_EXPERIENCE

We can assume that these strings show the relevance of features to the device where the app is installed. However, requesting a full list of features on the updated device proves that the assumption is wrong.

2. I would disagree with the ‘frosting versions’ as you can find similar data, but with values other than 1. The maximum value of this field that I have come across so far is 26.

3. I would disagree with the ‘С timestamp of the frosting creation’: I have been monitoring a specific app and noticed that this field value does not necessarily increase with every new version. It tends to be unstable and can become negative.

4. MinSdkLevel and VersionCode appear plausible.

Conclusion

In summary, a frosting block in the signature helps to precisely ascertain if a file has been distributed through an official store. I wasn’t able to derive any other benefit from this signature.

For the finale, here is an illustration from the ApkLab mobile sandbox report of how this information is applied:

❌
❌