Post

ARM64 Assembly for Beginner Pentesters

Introduction

Welcome to a tutorial focused on ARM64 assembly language for budding penetration testers! This blog will walk you through an ARM64 (AArch64) program designed to check if a number is prime, and explore the key assembly instructions that are integral to understanding ARM64 assembly programming. The blog is perfect for those interested in exploring Android-like devices or any system using the ARM64 architecture, which is common in mobile devices and IoT devices. Mastering this architecture will empower pentesters to better analyze and exploit ARM64-based systems.


Introduction to ARM64 Assembly Language and its Role in Pentesting

ARM64 (also known as AArch64) is a 64-bit architecture widely used in mobile devices, servers, and embedded systems. It has become increasingly popular in Android devices, making it crucial for penetration testers to learn ARM64 assembly. Unlike x86 or x64 architectures, ARM64 requires specific tools and knowledge that help in reverse engineering and exploiting ARM64-based systems.

In this blog, we will dive into an ARM64 program that checks whether a number is prime. Not only will you learn how the code works, but you’ll also understand key assembly instructions, their functions, and why they are important in the context of pentesting and reverse engineering.

Prerequisites: Setting Up the ARM64 Environment

Before diving into the code, let’s first ensure that you have the required tools installed to compile and run ARM64 assembly code on a Linux-based system.

Install the Required Tools:

Run the following commands in your terminal to install the necessary tools for compiling and running ARM64 code:

1
2
3
sudo apt install binutils-aarch64-linux-gnu
sudo apt install qemu-user
sudo apt install qemu qemu-user-static

Compile the Code:

To compile the ARM64 assembly program, use these commands:

1
2
aarch64-linux-gnu-as -o prime.o prime.s
aarch64-linux-gnu-ld -o prime prime.o

Run the Code:

Finally, to run your program on an ARM64 emulated environment, use the following command:

1
qemu-aarch64 ./prime

ARM64 Code Walkthrough: Prime Number Check

This code will return 1 if the number is prime else it will return 0. Now, let’s break down the code below and go over each instruction in detail.

.global _start
.global is_prime
.type is_prime, %function

_start:
    mov x0, #29           // Set x0 to 29 (testing with prime number)
    bl is_prime           // Branch to is_prime function

    mov x8, #93           // Syscall number for exit (Linux x64)
    svc #0                // Make the system call to exit with status in x0

is_prime:
    cmp x0, #2           // Compare n with 2
    blt not_prime         // If n < 2, it's not prime (branch to not_prime)

    cmp x0, #2
    beq is_prime_label    // If n == 2, it's prime (branch to is_prime_label)

    and x1, x0, #1       // Perform bitwise AND between n and 1
    cmp x1, #0           // If result is 0, the number is even
    beq not_prime         // If even, it's not prime (branch to not_prime)

    mov x1, #3           // Set x1 to 3 (starting divisor)

check_divisibility:
    cmp x1, x0           // Compare current divisor (x1) with n
    bge is_prime_label    // If x1 >= n, we've finished checking divisors

    udiv x2, x0, x1      // x2 = n / x1 (integer division)
    mul x3, x2, x1       // x3 = (n / x1) * x1 (to see if it's divisible)
    cmp x3, x0           // Compare x3 with n
    beq not_prime         // If n is divisible by x1, it's not prime (branch to not_prime)

    add x1, x1, #2       // Increment divisor by 2 (check next odd number)
    b check_divisibility // Repeat loop

is_prime_label:
    mov x0, #1           // Set x0 to 1 (return true: prime)
    ret

not_prime:
    mov x0, #0           // Set x0 to 0 (return false: not prime)
    ret

Breaking Down the Key Instructions

  1. mov:
    • The mov instruction is used to load an immediate value into a register. For example, mov x0, #29 sets the value of register x0 to 29, which is the number we want to check for primality.
    • This is a straightforward instruction to set up the initial value.
  2. bl (Branch with Link):
    • The bl instruction is used to branch to a function or label while saving the return address in the link register (lr). It’s essential for calling functions like is_prime.
    • For example, bl is_prime transfers control to the is_prime function.
  3. b (Branch):
    • The b instruction is a simple unconditional jump to a label. It’s used when you want to jump to another part of the code, such as b check_divisibility to repeat the loop.
    • Unlike bl, it doesn’t save the return address in a register.
  4. cmp (Compare):
    • The cmp instruction compares two values by subtracting them, but without storing the result. It affects the condition flags, which can then be used by subsequent branch instructions.
    • For example, cmp x0, #2 compares the value in register x0 with 2, setting flags for conditional branches like blt or beq.
  5. blt (Branch if Less Than):
    • The blt instruction branches if the result of the previous comparison was less than zero (signed comparison).
    • In the code, blt not_prime ensures that if x0 is less than 2, the program jumps to the not_prime label.
  6. beq (Branch if Equal):
    • The beq instruction branches if the values compared by the cmp instruction are equal (i.e., zero flag is set).
    • Example: beq is_prime_label branches if the number is exactly 2 (a prime number).
  7. bge (Branch if Greater or Equal):
    • The bge instruction branches if the first value is greater than or equal to the second value in a signed comparison.
    • Here, bge is_prime_label checks if the divisor has reached or exceeded the number being tested. If so, it concludes that the number is prime.
  8. udiv (Unsigned Division):
    • The udiv instruction performs unsigned integer division. In this case, udiv x2, x0, x1 calculates the quotient of x0 divided by x1 and stores the result in x2.
    • This is used to check if n is divisible by any divisor.
  9. mul (Multiply):
    • The mul instruction multiplies two registers and stores the result in the destination register. For instance, mul x3, x2, x1 calculates the product of x2 and x1.
    • This helps verify if the result of division multiplied back equals the original number (i.e., checking divisibility).

Conclusion: Why Understanding These Instructions Matters for Pentesters

In conclusion, mastering ARM64 assembly will enhance your reverse engineering and pentesting skills, especially in Android-like environments that utilize ARM64 processors. So, the next time you’re working with ARM64 assembly, remember these instructions, and you’ll have a deeper understanding of how ARM64-based devices work and how to leverage their weaknesses!

Happy hacking!

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