Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

This book teaches security analysis of Android applications (APK reversing, dynamic instrumentation, network traffic inspection and interception, etc.).

The source code is available here.

🚧 The book is still under construction. New chapters will be added and the existing ones might be modified.

Prerequisites

Rust

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ rustup target add aarch64-linux-android
$ rustup target add x86_64-linux-android

yara-x

$ cargo install yara-x-cli

udev rules

Adding the udev rules manually might not be necessary on certain distros.

$ lsusb
...
Bus 001 Device 067: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device (charging + debug)
...
$ echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", ATTR{idProduct}=="4ee7", MODE="0660", GROUP="plugdev"' | sudo tee /etc/udev/rules.d/51-android.rules
$ sudo usermod -aG plugdev $USER
$ sudo udevadm control --reload
$ sudo udevadm trigger

Android Studio

Download Android Studio (android-studio-quail1-patch2-linux.tar.gz).

$ sudo dnf install zlib.i686 ncurses-libs.i686 bzip2-libs.i686
$ sudo tar -xzf android-studio-quail1-patch2-linux.tar.gz -C /opt/
$ sudo ln -sf /opt/android-studio/bin/studio /usr/local/bin/android-studio

Note: check the Android SDK version of your phone and select the linker accordingly (e.g. aarch64-linux-android36-clang).

$ adb shell getprop ro.build.version.sdk
37
$ echo 'export ANDROID_HOME=$HOME/Android/Sdk' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/platform-tools' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/emulator' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin' >> ~/.zshrc
$ echo 'export PATH=$PATH:$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools | tail -1)' >> ~/.zshrc
$ echo 'export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$(ls $ANDROID_HOME/ndk | tail -1)' >> ~/.zshrc
$ echo 'export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang' >> ~/.zshrc
$ echo 'export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android36-clang' >> ~/.zshrc
$ source ~/.zshrc

After startup install additional tools:

  • More Actions –> SDK Manager –> SDK Tools
    • –> NDK (Side by side)
    • –> Android SDK Command-line Tools (latest)
    • –> CMake

Emulator

Note: google_apis_playstore does not allow adb root.

$ sdkmanager "system-images;android-36;google_apis;x86_64"
$ avdmanager create avd -n test -k "system-images;android-36;google_apis;x86_64"
$ android emulator list                                                         
test
$ android emulator start test
Emulator process 2673237 started, log file location: '/home/gemesa/.android/test/emulator.log'
Waiting for virtual device 'test' to fully start (242 seconds left)
Virtual device successfully started as 'emulator-5554'
$ adb devices
List of devices attached
emulator-5554	device
$ adb shell getprop ro.build.version.sdk
36
$ adb root
$ adb shell

pipx

$ sudo dnf install pipx

References

Android Developers

OWASP Mobile Application Security

Tools

KernelSU

Config

Magisk

Config

frida

Use case

Dynamically instrument the target application

Explanation

Hook certain Java or native functions to:

  • trace calls and print callstacks
  • inspect or modify their arguments or return values
  • change their implementation completely

Usage

Note: you need to set up frida first. Simplest case: rooted device/emulator + frida-server.

$ wget https://github.com/frida/frida/releases/download/17.19.0/frida-server-17.19.0-android-arm64.xz
$ xz -d frida-server-17.19.0-android-arm64.xz
$ chmod +x frida-server-17.19.0-android-arm64
$ adb push frida-server-17.19.0-android-arm64 /data/local/tmp/
$ adb shell
$ su
# cd /data/local/tmp
# ./frida-server-17.19.0-android-arm64 &
# exit
$ exit
$ pipx install frida-tools
$ frida-ps -U -a                     
 PID  Name             Identifier                             
----  ---------------  ---------------------------------------
3177  Camera           com.android.camera2                    
5027  Chrome           com.android.chrome                     
2997  Files            com.google.android.documentsui         
1783  Google           com.google.android.googlequicksearchbox
1783  Google           com.google.android.googlequicksearchbox
3791  Messages         com.google.android.apps.messaging      
3539  Personal Safety  com.google.android.apps.safetyhub      
2940  Phone            com.google.android.dialer              
4665  Photos           com.google.android.apps.photos         
1329  SIM Toolkit      com.android.stk                        
1443  Settings         com.android.settings

You can use frida-trace to auto-generate the default onEnter and onLeave handlers. These can be modified after if necessary.

Hook a native function (connect):

$ frida-trace -U -p 5027 -i "connect"
Instrumenting...                                                        
connect: Auto-generated handler at "/home/gemesa/git-repos/android-security-handbook/__handlers__/libc.so/connect.js"
Started tracing 1 function. Web UI available at http://localhost:44493/ 
Tip: Luma, the official Frida app, has frida-trace built in — with a Monaco handler editor and instruction-level tracing you can diff across runs. https://luma.frida.re/
           /* TID 0x1405 */
 35936 ms  connect(sockfd=0x102, addr=0x73a9b303f300, addrlen=0x1c)
 35937 ms     | connect(sockfd=0x103, addr=0x73acaabdb69e, addrlen=0x6e)
 35938 ms     | connect(sockfd=0x103, addr=0x73acaabdb69e, addrlen=0x6e)
...

/home/gemesa/git-repos/android-security-handbook/__handlers__/libc.so/connect.js:

/*
 * Auto-generated by Frida. Please modify to match the signature of connect.
 * This stub is currently auto-generated from manpages when available.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */

defineHandler({
  onEnter(log, args, state) {
    log(`connect(sockfd=${args[0]}, addr=${args[1]}, addrlen=${args[2]})`);
  },

  onLeave(log, retval, state) {
  }
});

Hook a Java function (android.net.Uri!parse):

$ frida-trace -U -p 5027 -j 'android.net.Uri!parse'
Instrumenting...                                                        
Uri.parse: Auto-generated handler at "/home/gemesa/git-repos/android-security-handbook/__handlers__/android.net.Uri/parse.js"
Started tracing 1 function. Web UI available at http://localhost:41025/ 
Tip: Luma, the official Frida app, has frida-trace built in — with a Monaco handler editor and instruction-level tracing you can diff across runs. https://luma.frida.re/
           /* TID 0x13a3 */
 11338 ms  Uri.parse("https://m.youtube.com/")
 11339 ms  <= "<instance: android.net.Uri, $className: android.net.Uri$StringUri>"
...

/home/gemesa/git-repos/android-security-handbook/__handlers__/android.net.Uri/parse.js:

/*
 * Auto-generated by Frida. Please modify to match the signature of Uri.parse.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */

defineHandler({
  /**
   * Called synchronously when about to call Uri.parse.
   *
   * @this {object} - The Java class or instance.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {array} args - Java method arguments.
   * @param {object} state - Object allowing you to keep state across function calls.
   */
  onEnter(log, args, state) {
    log(`Uri.parse(${args.map(JSON.stringify).join(', ')})`);
  },

  /**
   * Called synchronously when about to return from Uri.parse.
   *
   * See onEnter for details.
   *
   * @this {object} - The Java class or instance.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {NativePointer} retval - Return value.
   * @param {object} state - Object allowing you to keep state across function calls.
   */
  onLeave(log, retval, state) {
    if (retval !== undefined) {
      log(`<= ${JSON.stringify(retval)}`);
    }
  }
});

Anti-instrumentation/anti-hooking

Some hardened apps are capable of detecting the presence of frida. A cheap solution might be renaming frida-server and making it listen on a non-default port.

Host:

# mv ./frida-server-17.18.0-android-arm64 ol0
# ./ol0 -l 0.0.0.0:29436

Client:

$ frida-trace -H <host_ip>:29436 -p 5027 -j 'android.net.Uri!parse'

Client isolation

On some networks clients are isolated, meaning they cannot communicate to each other. Some possible solutions are listed below.

Convention:

  • client: pc
  • host: android
# wifi client isolation is enabled:
#
# client:
# adb forward tcp:29436 tcp:29436
# frida -H 127.0.0.1:29436 -n <app>
# host:
# ./ol0 -l 127.0.0.1:29436 &
# or:
# nohup ./ol0 -l 127.0.0.1:29436 > /dev/null 2>&1 &
#
# https://github.com/jpillora/chisel
# client:
# chisel server -p 8080 --reverse
# host:
# curl -LO https://github.com/jpillora/chisel/releases/download/v1.12.0/chisel_1.12.0_linux_arm64.gz
# gunzip chisel_1.12.0_linux_arm64.gz
# chmod +x chisel_1.12.0_linux_arm64
# chisel client CLIENT_IP:8080 R:29436:127.0.0.1:29436
# ./ol0 -l 127.0.0.1:29436 &
# or:
# nohup ./ol0 -l 127.0.0.1:29436 > /dev/null 2>&1 &
#
# USB tethering
#
# client:
# ip a --> enx... interface
# get gateway (android device) ip:
# ip route | grep enx
# frida -H ENX_GATEWAY_IP:29436 -n <app>
# host:
# Settings --> Network & Internet --> Hotspot & Tethering --> USB Tethering
# ./ol0 -l 0.0.0.0:29436 &
# or:
# nohup ./ol0 -l 0.0.0.0:29436 > /dev/null 2>&1 &
#
# wifi client isolation is disabled:
#
# client:
# frida -H <phone-wifi-ip>:29436 -n <app>
# host:
# ./ol0 -l 0.0.0.0:29436 &
# or:
# nohup ./ol0 -l 0.0.0.0:29436 > /dev/null 2>&1 &

Running frida-server in an improperly configured shell

In some cases (e.g. kernel exploits), we do not spawn a properly configured shell so we need to set up some environment variables manually.

# adb shell
# echo $BOOTCLASSPATH > /data/local/tmp/boot.txt
# echo $DEX2OATBOOTCLASSPATH > /data/local/tmp/dex2oat.txt
BOOTCLASSPATH="$(cat /data/local/tmp/boot.txt)" \
DEX2OATBOOTCLASSPATH="$(cat /data/local/tmp/dex2oat.txt)" \
ANDROID_DATA=/data \
ANDROID_ROOT=/system \
ANDROID_RUNTIME_ROOT=/apex/com.android.runtime \
ANDROID_TZDATA_ROOT=/apex/com.android.tzdata \
ANDROID_I18N_ROOT=/apex/com.android.i18n \
./ol0 -l 0.0.0.0:29436

Memory dumping

Use case

Capture the mapped memory of a process to recover values from RAM.

Explanation

The list of mapped regions is available through /proc/<pid>/maps. The content of these regions can be read via /proc/<pid>/mem. It is a good idea to freeze the process first, then dump the memory. For now, we only dump rw-p regions.

There are other solutions available for dumping memory, e.g. hexdump, fridump or gdb and lldb. The main problem is that attaching to a process with frida, gdb and lldb can be detected.

Usage

$ cargo build --release --target aarch64-linux-android
$ adb push target/aarch64-linux-android/release/dumpmem /data/local/tmp/

Phone:

# ps -ef | grep test                                                                                      
u0_a220       5542   465 0 22:02:57 ?     00:00:02 com.example.test
u0_a220       5984   465 1 22:53:24 ?     00:00:00 com.example.test:worker2
u0_a220       5985   465 1 22:53:24 ?     00:00:00 com.example.test:worker1
root          6093  5955 0 22:55:00 pts/1 00:00:00 grep test
# ./dumpmem -p com.example.test -p com.example.test:worker1                                               
Found PIDs: [5542, 5985]
Stopping PIDs: [5542, 5985]
Dumping rw-p regions...
Done: /data/local/tmp/memdump_5542
Dumping rw-p regions...
Done: /data/local/tmp/memdump_5985
Resuming PIDs: [5542, 5985]
# ls memdump_5542/bin | head -n 5                                                                         
2000000-e000000.bin
26000000-32000000.bin
4a000000-4a002000.bin
57e9108b0000-57e9108b1000.bin
6ffb4000-702a0000.bin
# head -n 5 memdump_5542/dumpmem.log                                                                      
02000000-0e000000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space]
26000000-32000000 rw-p 00000000 00:00 0                                  [anon:dalvik-free list large object space]
4a000000-4a002000 rw-p 00000000 00:00 0 
4a002000-4c002000 r--s 00000000 00:01 1040                               /memfd:jit-zygote-cache (deleted)
4c002000-4e002000 r-xs 02000000 00:01 1040                               [anon_shmem:dalvik-zygote-jit-code-cache]

Code

use std::{
    fs::{self, File},
    io::{self, Read, Seek, SeekFrom, Write},
    path::{Path, PathBuf},
    process::exit,
};

use std::sync::Mutex;

use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;

// https://docs.rs/clap/latest/clap/#example
use clap::Parser;

/// Dump the rw-p memory regions of an Android process.
#[derive(Parser)]
#[command(version, about, arg_required_else_help = true)]
struct Args {
    /// Process names to dump, e.g. -p com.example.test -p com.example.test:worker1.
    /// PIDs are also accepted: -p 1234.
    #[arg(short, long)]
    process: Vec<String>,

    /// Output directory. The output is written to <output>/memdump_<pid>/.
    #[arg(short, long, default_value = "/data/local/tmp")]
    output: PathBuf,
}

static STOPPED_PIDS: Mutex<Vec<i32>> = Mutex::new(Vec::new());

// Resume during cleanup whatever happens.
struct Resumer(Vec<Pid>);
impl Drop for Resumer {
    fn drop(&mut self) {
        let raw: Vec<i32> = self.0.iter().map(|p| p.as_raw()).collect();
        println!("Resuming PIDs: {raw:?}");
        for &p in &self.0 {
            _ = kill(p, Signal::SIGCONT);
        }
        STOPPED_PIDS.lock().unwrap().clear();
    }
}

fn main() {
    let args = Args::parse();
    if let Err(e) = run(&args.process, &args.output) {
        eprintln!("error: {e}");
        exit(1);
    }
}

fn run(names: &[String], out_base: &Path) -> io::Result<()> {
    let mut pids: Vec<i32> = Vec::new();
    for name in names {
        let pid_raw = match name.parse::<i32>() {
            Ok(pid) => pid,
            Err(_) => pidof(name)?.ok_or_else(|| {
                io::Error::new(io::ErrorKind::NotFound, format!("{name} not running?"))
            })?,
        };
        pids.push(pid_raw);
    }

    println!("Found PIDs: {pids:?}");

    // https://crates.io/crates/ctrlc
    ctrlc::set_handler(move || {
        let stopped = STOPPED_PIDS.lock().unwrap();
        println!("Resuming PIDs: {stopped:?}");
        for &p in stopped.iter() {
            _ = kill(Pid::from_raw(p), Signal::SIGCONT);
        }

        // https://man7.org/linux/man-pages/man7/signal.7.html
        // https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
        // 128 + SIGINT = 130
        exit(130);
    })
    .expect("failed to install Ctrl+C handler");

    *STOPPED_PIDS.lock().unwrap() = pids.clone();

    let _resumer = Resumer(pids.iter().map(|&p| Pid::from_raw(p)).collect());
    println!("Stopping PIDs: {pids:?}");
    for &p in &pids {
        kill(Pid::from_raw(p), Signal::SIGSTOP)?;
    }

    for &pid_raw in &pids {
        // output0 = <output>/memdump_<pid>/bin/*.bin
        // output1 = <output>/memdump_<pid>/dumpmem.log
        let out = out_base.join(format!("memdump_{pid_raw}"));
        let bin = out.join("bin");
        let log_path = out.join("dumpmem.log");
        fs::create_dir_all(&bin)?;
        let mut log = File::create(&log_path)?;

        let maps = fs::read_to_string(format!("/proc/{pid_raw}/maps"))?;
        // Good practice: use write_all for bytes we already have.
        log.write_all(maps.as_bytes())?;

        println!("Dumping rw-p regions...");
        let mut mem = File::open(format!("/proc/{pid_raw}/mem"))?;

        // 55fc5b37a000-55fc5b425000 rw-p 00000000 00:00 0    [heap]
        // 7fcd00021000-7fcd04000000 ---p 00000000 00:00 0
        // https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map
        for region in maps.lines().filter_map(parse_region) {
            let (start, end) = region;
            writeln!(log, "{start:x}-{end:x}")?;

            let len = (end - start) as usize;
            let mut buf = vec![0; len];

            // Straight to the region and read it.
            mem.seek(SeekFrom::Start(start))?;
            match mem.read_exact(&mut buf) {
                Ok(()) => {
                    let path = bin.join(format!("{start:x}-{end:x}.bin"));
                    fs::write(&path, &buf)?;
                    writeln!(log, "wrote {len} bytes -> {}", path.display())?;
                }
                Err(e) => {
                    writeln!(log, "skip {start:x}-{end:x}: {e}")?;
                }
            }
        }

        writeln!(log, "Done: {}", out.display())?;
        println!("Done: {}", out.display());
    }

    Ok(())
}

fn pidof(name: &str) -> io::Result<Option<i32>> {
    // https://doc.rust-lang.org/std/fs/fn.read_dir.html#examples
    for entry in fs::read_dir("/proc")? {
        let entry = entry?;
        let fname = entry.file_name();
        let Some(pid) = fname.to_str().and_then(|s| s.parse::<i32>().ok()) else {
            continue;
        };

        // argv[0] = package name
        if let Ok(cmdline) = fs::read(format!("/proc/{pid}/cmdline")) {
            let argv0 = cmdline.split(|&b| b == 0).next().unwrap_or(&[]);
            if argv0 == name.as_bytes() {
                return Ok(Some(pid));
            }
        }
    }
    Ok(None)
}

// 55fc5b37a000-55fc5b425000 rw-p 00000000 00:00 0    [heap]
fn parse_region(line: &str) -> Option<(u64, u64)> {
    let mut parts = line.split_whitespace();
    let range = parts.next()?;
    let perms = parts.next()?;
    if perms != "rw-p" {
        return None;
    };
    let (s, e) = range.split_once('-')?;
    let start = u64::from_str_radix(s, 16).ok()?;
    let end = u64::from_str_radix(e, 16).ok()?;
    Some((start, end))
}

View on GitHub.

Memory search

Use case

Quickly search the previously dumped memory regions for a specific value in multiple encodings.

Explanation

We convert the search value to multiple byte patterns: ASCII, UTF-16 (LE/BE), 32/64-bit ints (LE/BE) and raw hex (+ reversed). Then we search for all of them in the dumped .bin files. yara-x does the search based on the rule generated by mkrule.

Alternatively, r2 could be used as well. But yr is significantly faster.

Usage

$ cargo build --release
$ target/release/mkrule 285735461 > rule.yar
$ cat rule.yar
rule searchmem
{
    strings:
        $ascii        = "285735461" ascii nocase
        $utf16_le     = { 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00 }
        $utf16_be     = { 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 }
        $int32_le     = { 25 fa 07 11 }
        $int32_be     = { 11 07 fa 25 }
        $int64_le     = { 25 fa 07 11 00 00 00 00 }
        $int64_be     = { 00 00 00 00 11 07 fa 25 }
        $hex          = { 28 57 35 46 1? }
        $hex_rev      = { 1? 46 35 57 28 }
    condition:
        any of them
}
$ yr scan rule.yar memdump_5542/bin -s
searchmem memdump_5542/bin/2000000-e000000.bin
0x2d02bc:9:$ascii: 285735461
0x2d02e8:9:$ascii: 285735461
0x2d0304:9:$ascii: 285735461
0x2d04b4:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d04d4:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d04f4:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d0515:18:$utf16_le: 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31 00
0x2d04b3:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d04d3:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d04f3:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d0514:18:$utf16_be: 00 32 00 38 00 35 00 37 00 33 00 35 00 34 00 36 00 31
0x2d0564:4:$int32_le: 25 fa 07 11
0x2d05e4:4:$int32_le: 25 fa 07 11
0x2d05a4:4:$int32_be: 11 07 fa 25
0x2d0630:4:$int32_be: 11 07 fa 25
0x2d05e4:8:$int64_le: 25 fa 07 11 00 00 00 00
0x2d062c:8:$int64_be: 00 00 00 00 11 07 fa 25
0x2d0644:5:$hex: 28 57 35 46 10
0x2d065c:5:$hex_rev: 10 46 35 57 28
$ cat memdump_5542/dumpmem.log | grep -E "0*2000000-0*e000000 rw-p"
02000000-0e000000 rw-p 00000000 00:00 0                                  [anon:dalvik-main space]

Code

use std::process::exit;

fn print_string(label: &str, body: String) {
    // format specifiers:
    // https://doc.rust-lang.org/std/fmt/
    println!("        ${label:12} = {body}")
}

// deadbeef -> { DE AD BE EF }
fn hex(bytes: &[u8]) -> String {
    let pairs: Vec<String> = bytes.iter().map(|b| format!("{b:02x}")).collect();
    format!("{{ {} }}", pairs.join(" "))
}

// "285735461" --> ["28", "57", "35", "46", "1?"]
fn parse_hex(val: &str) -> Option<Vec<String>> {
    let mut pairs: Vec<String> = Vec::new();
    for i in (0..val.len()).step_by(2) {
        let pair = val.get(i..(i + 2).min(val.len()))?;
        if !pair.bytes().all(|b| b.is_ascii_hexdigit()) {
            return None;
        }
        let pair = pair.to_ascii_lowercase();
        if pair.len() == 1 {
            pairs.push(format!("{pair}?"));
        } else {
            pairs.push(pair);
        }
    }
    Some(pairs)
}

/*
rule searchmem
{
    strings:
        $ascii        = "1234" ascii nocase
        $utf16_le     = { 31 00 32 00 33 00 34 00 }
        $utf16_be     = { 00 31 00 32 00 33 00 34 }
        $int32_big    = { 00 00 04 D2 }
        $int32_little = { D2 04 00 00 }
        $int64_big    = { 00 00 00 00 00 00 04 D2 }
        $int64_little = { D2 04 00 00 00 00 00 00 }
        $hex          = { 12 34 }
        $hex_reversed = { 34 12 }
    condition:
        any of them
}
*/
fn print_rule(val: &str) {
    println!("rule searchmem");
    println!("{{");
    println!("    strings:");

    print_string("ascii", format!("\"{val}\" ascii nocase"));

    let utf16_le: Vec<u8> = val.encode_utf16().flat_map(u16::to_le_bytes).collect();
    let utf16_be: Vec<u8> = val.encode_utf16().flat_map(u16::to_be_bytes).collect();

    print_string("utf16_le", hex(&utf16_le));
    print_string("utf16_be", hex(&utf16_be));

    if let Ok(n) = val.parse::<u32>() {
        print_string("int32_le", hex(&n.to_le_bytes()));
        print_string("int32_be", hex(&n.to_be_bytes()));
    }

    if let Ok(n) = val.parse::<u64>() {
        print_string("int64_le", hex(&n.to_le_bytes()));
        print_string("int64_be", hex(&n.to_be_bytes()));
    }

    if let Some(pairs) = parse_hex(val) {
        let mut rev = pairs.clone();
        rev.reverse();
        print_string("hex", format!("{{ {} }}", pairs.join(" ")));
        print_string("hex_rev", format!("{{ {} }}", rev.join(" ")));
    }

    println!("    condition:");
    println!("        any of them");
    println!("}}");
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.len() != 1 || args[0].is_empty() {
        println!("usage: mkrule <value>");
        println!("e.g.: mkrule 1234 > rule.yar");
        exit(1);
    }

    print_rule(&args[0]);
}

View on GitHub.

Test app

Use case

This app can be used to test tools such as:

Usage

$ cd test/test-app
$ ./gradlew assemble
$ adb install app/build/outputs/apk/debug/app-debug.apk
$ adb shell am start -n com.example.test/.MainActivity
$ adb shell ps -ef | grep test                         
u0_a217       3585   465 0 19:28:56 ?     00:00:01 com.example.test
u0_a217       3604   465 0 19:28:56 ?     00:00:00 com.example.test:worker1
u0_a217       3605   465 0 19:28:56 ?     00:00:00 com.example.test:worker2