Home Deploy and Destroy Featuring Certificates
Post
Cancel

Deploy and Destroy Featuring Certificates

Intro

Setting up devices for security research deployment often takes longer than the testing itself. The time required depends on the complexity of device configuration. For example, if you’re using an Android Virtual Device (AVD) and destroying it after each engagement, it can be time-consuming to set it up again for further testing. This is just some tooling i wrote to automate this process and some notes on it.

Standard Setup

A number of manual processes are required for testing a testing device to be useful for android application testing, one big component is installing the Frida-server binary and adding the burp suite CA certificate, especially on a android 14+ devices and ensuring that its all correctly configured.

What, Why, Where?

Android 14 introduces significant changes to how certificate authorities (CAs) are handled on devices, impacting security testing tools like Burp Suite. Here’s what security professionals need to know about these changes.

The Previous Approach (Android 13 and Earlier)

On earlier Android versions, installing a custom certificate authority involved:

  1. Downloading the certificate in DER format
  2. Converting it to PEM format (which Android supports)
  3. Installing it to the system’s certificate store (/system/etc/security/cacerts/)
  4. Successfully Intercepting SSL/TLS traffic

What’s Changed in Android 14?

The major change in Android 14 relates to how the system manages trusted certificate authorities. The certificate store has moved to Android’s APEX (Android Pony EXpress) directory structure instead /system/etc/security/cacerts/ directory of the as of Android 10 which is part of Google’s efforts to modularize system components for better security and updatability.

Pre 14 Android Certificate Install.

To install the burp suite certificate, Its possible to download the certificate by enabling the Proxy listeners interface to be exposed and then downloading burpsuite PortSwigger CA certificate as demostrated in the screenshots below.

image

From here, you can browse to the interface’s exposed port and download the Burp certificate.

image

However, before using it on the Android device, you must convert it from a .der file to a .pem file. Perform the following commands:

1
2
openssl x509 -inform DER -in cacert.der -out cacert.pem
cp cacert.der $(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0

These commands will convert the certificate to a valid PEM key which then can installed by pushing the .pem file with adb push to /sdcard and installing the certificate. However this changed in Android 14 as mentioned above.

+14 Android Certificate Manual Install

The setup for this testing device uses Android Studio’s AVD (Android Virtual Device) with Android 14 UpsideDownCake version, without Google APIs. We avoid using the Google APIs version because root access isn’t available in production builds.

image

Now by browsing to Security & PrivacyEncryption & credentialsTrusted Credentials. As we can see, the PortSwigger Burp CA certificate is not present in the trusted credentials store, and we can’t intercept the traffic. However, we can follow these steps to install the Burp certificate into the trusted credentials, in the adb shell we can perform the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Disable SELinux enforcement
setenforce 0

# Remount /apex with exec permissions
mount -o remount,exec /apex

# Backup conscrypt directory
cp -r -p /apex/com.android.conscrypt /apex/com.android.conscrypt-bak

# Unmount the conscrypt directory
umount -l /apex/com.android.conscrypt

# Remove the original conscrypt directory
rm -rf /apex/com.android.conscrypt

# Push new certificate to backup location
adb push 9a5ba575.0 /apex/com.android.conscrypt-bak/cacerts/9a5ba575.0

# Restart system server to apply changes
killall system_server

# Replace the original conscrypt directory with the modified backup
mv /apex/com.android.conscrypt-bak /apex/com.android.conscrypt

Once this performed the PortSwigger CA certificate is installed into the Trusted Credentials section of the Android device.

image

We can now intercept SSL/TLS traffic, as shown in the screenshot below.

image

Automate All the things

Due to the tedious nature of destroying and redeploying Android devices, along with configuring the Portswigger CA for Android 14+ devices, I decided to streamline the process. Setting up each device manually gets repetitive, especially when you’re doing security research and frequently resetting environments.

This script automates the setup and deployment of tools for security research, offering several capabilities that make the process significantly more efficient.

  • Downloads, converts, and installs the Burp Suite certificate for pre-Android 13 versions
  • Downloads, converts, and installs the Burp Suite certificate as an APEX file for Android 14+ (root access required)
  • Downloads and installs Frida Server on the device
  • Downloads and installs device tools such as JTrace

One time to keep in mind is that the certificate is not persistent across reboots, and thus reinstalling it is required.

This script was rewritten on OSX using Android studio +14 AVD emulator.

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
import adbutils
from time import sleep
import requests
import os
from adbutils import adb
from rich.progress import track
from rich.progress import wrap_file
import lzma 
from rich.console import Console
from rich.progress import Progress

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
import hashlib
import subprocess

console = Console()

# Color logging because im lazy.
def download_with_progress_bar(url, filename):
    """
    Download a file with a progress bar using the rich library.

    Parameters:
    - url: The URL to download the file from.
    - filename: The filename to save the downloaded file as.
    """
    with requests.get(url, stream=True) as r:
        r.raise_for_status()  # Raise an exception for incorrect responses
        total_size_in_bytes = int(r.headers.get('content-length', 0))
        with Progress() as progress:
            task_id = progress.add_task("Download", total=total_size_in_bytes, visible=True)
            with open(filename, 'wb') as f:
                for chunk in r.iter_content(chunk_size=1024):
                    f.write(chunk)
                    progress.update(task_id, advance=len(chunk))

def log_bold(message, color="white"):
    """
    Log a message in bold with the specified color.
    
    :param message: The message to log.
    :param color: The color of the text.
    """
    console.log(f"[bold {color}]{message}[/bold {color}]")

import argparse

try:
    from frida import __version__ as FRIDA_VERSION
except ImportError:
    print("Frida is missing, please install it with: pip install frida")
    exit(1)

console.print("[bold red]Frida version: " + FRIDA_VERSION + "[/bold red]")

def convert_der_to_pem(der_file_path, pem_file_path):
    # Read the DER file
    with open(der_file_path, 'rb') as f:
        der_data = f.read()

    # Load the DER certificate
    certificate = x509.load_der_x509_certificate(der_data, default_backend())

    # Convert to PEM
    pem_data = certificate.public_bytes(serialization.Encoding.PEM)

    # Write the PEM file
    with open(pem_file_path, 'wb') as f:
        f.write(pem_data)

    console.log("[bold green]Certificate converted to PEM[/bold green]")

def get_device_arch(id=None):
    arch = None
    device = adb.device(serial=id)
    if device:
        arch = device.shell(["getprop", "ro.product.cpu.abi"])
        if arch == "arm64-v8a":
            console.log("[bold green]Device is arm64-v8a [bold green]")
        elif arch == "armeabi-v7a":
            console.log("[bold green]Device is armeabi-v7a..[/bold green]")
        elif arch == "x86":
            print("Device is x86")
        elif arch == "x86_64":
            console.log("[bold white]Device is[/bold white] [bold green]x86_64...[/bold green]")
        else:
            print("Unknown Arch: " + arch)
    return arch

def download_extract_frida(url=None):
    log_bold("Downloading Frida Server..", "white")
    device = adb.device(serial="emulator-5554")
    r = requests.get(url, stream=True)
    if r.status_code == 200:
        arch_name = "frida-server" + ".xz"
        r.raw.decode_content = True
        extracted_name = "frida-server"  # Assuming you want to remove .xz and keep the name
        # use Rich progress bar to show progress when downloading
        resp_size = int(r.headers['content-length'])

        with wrap_file(r, resp_size) as wrapped:
            with open(arch_name, 'wb') as f:
                for chunk in r.iter_content(chunk_size=1024):
                    f.write(chunk)
                    f.flush()

        print("Extracting Frida Server..")
        with lzma.open(arch_name) as compressed:
            with open(extracted_name, 'wb') as extracted_file:
                extracted_file.write(compressed.read())
        console.log("[bold yellow]Pushing Frida Server to device - /data/local/tmp/frida-server [/bold yellow]")
        device.sync.push("frida-server", "/data/local/tmp/frida-server")
        console.log("[bold green]Setting permissions..[/bold green]")
        device.shell(["chmod", "755", "/data/local/tmp/frida-server"])
        console.log("[bold green]Starting Frida Server..[/bold green]")
        device.shell(["/data/local/tmp/frida-server", "&"])
        print("Frida Server started")
        # Return True if everything went well
        return True
    else:
        print("Something went wrong")

def install_burp_cert_device_old():
    log_bold("Pre Android 13+ device, Installing burp certificate into cert storage.", "white")
    certifcate = get_burp_certificate()
    if certifcate:
        convert_cert()
        device = adb.device(serial="emulator-5554")
        log_bold("Pushing required files to /sdcard..", "white")
        device.push("cacert.der", "/sdcard/cacert.der")
        device.push("9a5ba575.0", "/sdcard/9a5ba575.0")
        device.push("cacert.pem", "/sdcard/cacert.pem")
        log_bold("Switching to root..", "white")
        is_root = device.root()
        if is_root:
            try:
                log_bold("Root access granted..", "green")
            # Create a directory in /data/local/tmp
                log_bold("Creating directory in /data/local/tmp..", "white")
                device.shell(["su", "-c", "mkdir -m 700 /data/local/tmp/setup"])
            # Copy the cacerts to /data/local/tmp/setup
                log_bold("Copying cacerts to /data/local/tmp/setup..", "white")
                device.shell(["su", "-c", "cp /system/etc/security/cacerts/* /data/local/tmp/setup"])
                log_bold("Creating inmemory mountfs", "yellow")
                device.shell(["su", "-c", "mount -t tmpfs tmpfs /system/etc/security/cacerts"])
                device.shell(["su", "-c", "cp /data/local/tmp/setup/* /system/etc/security/cacerts"])
            # Moving the 9a5ba575.0 to /system/etc/security/cacerts
                log_bold("Moving 9a5ba575.0 to /system/etc/security/cacerts", "yellow")
                device.shell(["su", "-c", "mv /sdcard/9a5ba575.0 /system/etc/security/cacerts/"])
            # Permissions setup
                log_bold("Setting permissions..", "white")
                device.shell(["su", "-c", "chown root:root /system/etc/security/cacerts/*"])
                device.shell(["su", "-c", "chmod 644 /system/etc/security/cacerts/*"])
            # Chchon 
                log_bold("Chcon..", "white")
                device.shell(["su", "-c", "chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*"])
                log_bold("[ALERT] The certificate is not Persitent, you need to install it again after reboot", "red")
            except Exception as e:
                print(f"Error during Burp certificate installation: {e}")
        else:
            print("Root access not granted, please check the device")

def frida_url(arch=None):
    # If the arch is arm64-v8a we need to strip the v8a
    if arch == "arm64-v8a":
        arch = "arm64"
    
    base_url = "https://github.com/frida/frida/releases/download/{}/frida-server-{}-android-{}.xz"
    return base_url.format(FRIDA_VERSION, FRIDA_VERSION, arch)

def install_burp_cert_device():
    device = adb.device(serial="emulator-5554")
    print("Switching to root..")
    device.root()
    user = device.shell(["whoami"])
    print(f"User: {user}")
    if user.strip() != "root":
        print("Something went wrong, we are unable to mount the tmpfs partition")
    else:
        print("Disable SELinux..")
        device.shell(["su", "-c", "setenforce 0"])
        print("Remounting /apex..")
        device.shell(["su", "-c", "mount -o remount,exec /apex"])
        print("Making a copy of the current conscrypt APEX contents..")
        device.shell(["su", "-c", "cp -r -p /apex/com.android.conscrypt /apex/com.android.conscrypt-bak"])
        print("Unmounting /apex..")
        device.shell(["su", "-c", "umount -l /apex/com.android.conscrypt"])
        print("Removing /apex/com.android.conscrypt..")
        device.shell(["su", "-c", "rm -rf /apex/com.android.conscrypt"])
        # We need to put 9a5ba575.o in /apex/cacerts to make it work
        print("Putting 9a5ba575.o in /apex/cacerts..")
        # Note: The push operation does not use shell, so it's not affected by 'su -c'
        device.sync.push("9a5ba575.0", "/apex/com.android.conscrypt-bak/cacerts/9a5ba575.0")
        print("Soft userspace reboot to get everything back into a consistent state..")
        device.shell(["su", "-c", "killall system_server"])

        print("Putting contents of conscrypt APEX on /apex tmpfs mount..")
        device.shell(["su", "-c", "mv /apex/com.android.conscrypt-bak /apex/com.android.conscrypt"])

def install_device_tools(arch, tools_dir='tools'):
    """
    Install device tools, including jtrace.

    Parameters:
    - arch: The architecture for which the tools are being installed (unused in current implementation).
    - tools_dir: The directory to store the tools.
    """
    jtrace_url = "http://newandroidbook.com/tools/jtrace.tgz"
    jtrace_name = "jtrace.tgz"

    bdsm_url = "https://newandroidbook.com/tools/bdsm.tgz"
    bdsm_name = "bdsm.tgz"

    # Ensure the tools directory exists
    if not os.path.exists(tools_dir):
        os.makedirs(tools_dir)

    log_bold("Arch: " + arch, "white")

    # Define the path to save the downloaded file
    jtrace_path = os.path.join(tools_dir, jtrace_name)
    

    # Download jtrace with a progress bar
    print(f"Downloading {jtrace_name} to {tools_dir}...")
    download_with_progress_bar(jtrace_url, jtrace_path)
    # push the jtrace tgz to the device
    log_bold("Pushing jtrace to the device...", "white")
    device = adb.device(serial="emulator-5554")
    device.sync.push(jtrace_path, "/data/local/tmp/jtrace.tgz")
    # extract the jtrace tgz
    log_bold("Extracting jtrace on the device...", "white")
    device.shell(["tar", "-xvzf", "/data/local/tmp/jtrace.tgz", "-C", "/data/local/tmp/"])
    # set permissions
    log_bold("Setting permissions...", "white")
    device.shell(["chmod", "755", "/data/local/tmp/"])

    # Define the path to save the downloaded file
    bsdm_path = os.path.join(tools_dir, bdsm_name)
    # Download bdsm with a progress bar
    print(f"Downloading {bdsm_name} to {tools_dir}...")
    download_with_progress_bar(bdsm_url, bsdm_path)
    # push the bdsm tgz to the device
    log_bold("Pushing bdsm to the device...", "white")
    device.sync.push(bsdm_path, "/data/local/tmp/bdsm.tgz")
    # extract the bdsm tgz
    log_bold("Extracting bdsm on the device...", "white")
    device.shell(["tar", "-xvzf", "/data/local/tmp/bdsm.tgz", "-C", "/data/local/tmp/"])
    # set permissions
    log_bold("Setting permissions...", "white")
    device.shell(["chmod", "755", "/data/local/tmp/"])
    

def get_device():

    for device in adb.device_list():
        device_info = device.info
        serial = device.serial
        console.log(f"[bold white]Device Serial: [bold green]{serial}[/bold green][/bold white]")    
        console.log(f"[bold white]Device Info: [bold green]{device_info}[/bold green][/bold white]")
        return serial

def get_device_serial():
    for device in adb.device_list():
        device_info = device.info
        device_info['android_version'] = device.shell(["getprop", "ro.build.version.release"]).strip()
        console.log(f"[bold white]Android Version: [bold green]{device_info['android_version']}[/bold green][/bold white]")
        return device_info['android_version']

def convert_cert_hash(der_path, pem_path):
    # Read in the DER formatted certificate
    with open(der_path, 'rb') as f:
        der_data = f.read()

    # Convert DER to PEM
    certificate = x509.load_der_x509_certificate(der_data, default_backend())
    pem_data = certificate.public_bytes(serialization.Encoding.PEM)

    # Write the PEM formatted certificate
    with open(pem_path, 'wb') as f:
        f.write(pem_data)
    print("Certificate converted to PEM format.")

    # This is where we try to replicate the OpenSSL subject hash
    # Extract the subject as a DER-encoded value
    subject_der = certificate.subject.public_bytes(serialization.Encoding.DER)
    
    # Hash the DER-encoded subject using MD5 (used by `-subject_hash_old`)
    subject_hash = hashlib.md5(subject_der).hexdigest()
    
    # The first 8 characters of the hash correspond to the subject hash
    hash_filename = subject_hash[:8] + '.0'
    
    with open(hash_filename, 'wb') as f:
        f.write(pem_data)
    print(f"Certificate renamed to subject hash: {hash_filename}")

def get_burp_certificate():
    r = requests.get('http://burp/cert', proxies={'http': 'http://127.0.0.1:8080'})
    with open('cacert.der', 'wb') as f:
        # Save the certificate in the current directory
        f.write(r.content)
        f.close()
    print("Certificate saved in cacert.der")
    return True

def convert_cert():  
    os.system('openssl x509 -inform DER -in cacert.der -out cacert.pem')
    print("Certificate converted to cacert.pem")
    os.system('cp cacert.der $(openssl x509 -inform PEM -subject_hash_old -in cacert.pem |head -1).0')
    print("Certificate converted to cacert hash")

if __name__ == '__main__':
    try:
        parser = argparse.ArgumentParser(description='PwnPhone Setup')
        parser.add_argument('--burp', help='Download Burp Certificate', action='store_true')
        parser.add_argument('--frida', help='Download Frida Server', action='store_true')
        parser.add_argument('--device', help='Get Device Info', action='store_true')
        parser.add_argument('--install', help='Install Burp Certificate', action='store_true')
        parser.add_argument('--arch', help='Get Device Architecture', action='store_true')
        parser.add_argument('--tools', help='Install Device Tools', action='store_true')
        parser.add_argument('--all', help="Installs all the tools and certificates", action='store_true')
        args = parser.parse_args()
        console = Console()
        if args.device:
            console.log("[bold green]Getting Device Info...[/bold green]")
            get_device()

        elif args.all:
            serial = get_device()
            arch = get_device_arch(id=serial)
            try:
                burp = get_burp_certificate()
                install_burp_cert_device()
                frida_download_url = frida_url(arch=arch) 
                print(frida_download_url)
                download_extract_frida(url=frida_download_url)
                install_device_tools(arch=arch)
            except Exception as e:
                console.log(f"Error during All installation: {e}")
        
        elif args.tools:
            console.log("[bold green]Installing Device Tools...[/bold green]")
            serial = get_device()
            arch = get_device_arch(id=serial)
            install_device_tools(arch=arch)
        
        elif args.arch:
            console.log("[bold green]Getting Device Architecture...[/bold green]")
            serial = get_device()
            get_device_arch(id=serial)

        elif args.install and args.frida:
            console.log("[bold green]Installing Burp Cert and Frida..[/bold green]")
            burp = get_burp_certificate()
            install_burp_cert_device()
            frida_download_url = frida_url(arch="arm64")  # Corrected function call
            print(frida_download_url)
            download_extract_frida(url=frida_download_url)

        elif args.burp:
            console.log("[bold green]Downloading Burp Certificate..[/bold green]")
            burp = get_burp_certificate()
            if burp:
                try:
                    version = get_device_serial()
                    if int(version) >= 14:
                        #convert_cert_hash("cacert.der", "cacert.pem")
                        convert_cert()
                        install_burp_cert_device()
                    else:
                        console.log("[bold green]Installing Burp Cert on older device...[/bold green]")
                        install_burp_cert_device_old()
                except Exception as e:
                    console.log(f"Error during Burp certificate installation: {e}")

        elif args.install:
            console.log("[bold green]Installing Burp Cert..[/bold green]")
            burp = get_burp_certificate()
            if burp:
                try:
                    convert_der_to_pem("cacert.der", "cacert.pem")
                    install_burp_cert_device()
                except Exception as e:
                    console.log(f"Error during Burp certificate installation: {e}")

        elif args.frida:
            serial = get_device()
            arch = get_device_arch(id=serial)
            # Reset the adb as root to install frida
            log_bold("Switching to root..", "white")
            device = adb.device(serial=serial)
            try:
                device.root()
            except Exception as e:
                console.log(f"Error during root reset: {e}")
            console.log("[bold green]Installing Frida..[/bold green]")
            frida_download_url = frida_url(arch=arch)  # Corrected function call
            print(frida_download_url)
            download_extract_frida(url=frida_download_url)
        else:
            console.log("No valid command found. Use --help for more information.")

    except Exception as e:
        console.log(f"[bold red]An error occurred: {e}[/bold red]")

While nothing in this script is groundbreaking, I find it quite useful for destroying and redeploying emulator devices for various testing scenarios or recovering from broken states. These commands can save significant time when working with Android emulators regularly.

This concludes my 2024 blog post, k bye.

This post is licensed under CC BY 4.0 by the author.