macOS Library Injection

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

caution

El código de dyld es de código abierto y se puede encontrar en https://opensource.apple.com/source/dyld/ y se puede descargar un tar usando una URL como https://opensource.apple.com/tarballs/dyld/dyld-852.2.tar.gz

Proceso Dyld

Echa un vistazo a cómo Dyld carga bibliotecas dentro de binarios en:

macOS Dyld Process

DYLD_INSERT_LIBRARIES

Esto es como el LD_PRELOAD en Linux. Permite indicar a un proceso que se va a ejecutar que cargue una biblioteca específica desde una ruta (si la variable de entorno está habilitada).

Esta técnica también puede ser utilizada como una técnica ASEP ya que cada aplicación instalada tiene un plist llamado "Info.plist" que permite la asignación de variables ambientales usando una clave llamada LSEnvironmental.

note

Desde 2012 Apple ha reducido drásticamente el poder de DYLD_INSERT_LIBRARIES.

Ve al código y verifica src/dyld.cpp. En la función pruneEnvironmentVariables puedes ver que las variables DYLD_* son eliminadas.

En la función processRestricted se establece la razón de la restricción. Al revisar ese código puedes ver que las razones son:

  • El binario es setuid/setgid
  • Existencia de la sección __RESTRICT/__restrict en el binario macho.
  • El software tiene derechos (runtime endurecido) sin el derecho com.apple.security.cs.allow-dyld-environment-variables
    • Verifica los derechos de un binario con: codesign -dv --entitlements :- </path/to/bin>

En versiones más actualizadas puedes encontrar esta lógica en la segunda parte de la función configureProcessRestrictions. Sin embargo, lo que se ejecuta en versiones más nuevas son los chequeos iniciales de la función (puedes eliminar los ifs relacionados con iOS o simulación ya que esos no se usarán en macOS).

Validación de Bibliotecas

Incluso si el binario permite usar la variable de entorno DYLD_INSERT_LIBRARIES, si el binario verifica la firma de la biblioteca para cargarla, no cargará una personalizada.

Para cargar una biblioteca personalizada, el binario necesita tener uno de los siguientes derechos:

o el binario no debería tener la bandera de runtime endurecido o la bandera de validación de bibliotecas.

Puedes verificar si un binario tiene runtime endurecido con codesign --display --verbose <bin> verificando la bandera runtime en CodeDirectory como: CodeDirectory v=20500 size=767 flags=0x10000(runtime) hashes=13+7 location=embedded

También puedes cargar una biblioteca si está firmada con el mismo certificado que el binario.

Encuentra un ejemplo sobre cómo (ab)usar esto y verifica las restricciones en:

macOS Dyld Hijacking & DYLD_INSERT_LIBRARIES

Secuestro de Dylib

caution

Recuerda que las restricciones de Validación de Bibliotecas anteriores también se aplican para realizar ataques de secuestro de Dylib.

Al igual que en Windows, en MacOS también puedes secuestro de dylibs para hacer que aplicaciones ejecuten código arbitrario (bueno, en realidad desde un usuario regular esto podría no ser posible ya que podrías necesitar un permiso de TCC para escribir dentro de un paquete .app y secuestrar una biblioteca).
Sin embargo, la forma en que las aplicaciones de MacOS cargan bibliotecas es más restringida que en Windows. Esto implica que los desarrolladores de malware aún pueden usar esta técnica para sigilo, pero la probabilidad de poder abusar de esto para escalar privilegios es mucho menor.

Primero que nada, es más común encontrar que los binarios de MacOS indican la ruta completa a las bibliotecas a cargar. Y segundo, MacOS nunca busca en las carpetas de $PATH para bibliotecas.

La parte principal del código relacionado con esta funcionalidad está en ImageLoader::recursiveLoadLibraries en ImageLoader.cpp.

Hay 4 comandos de encabezado diferentes que un binario macho puede usar para cargar bibliotecas:

  • El comando LC_LOAD_DYLIB es el comando común para cargar un dylib.
  • El comando LC_LOAD_WEAK_DYLIB funciona como el anterior, pero si el dylib no se encuentra, la ejecución continúa sin ningún error.
  • El comando LC_REEXPORT_DYLIB proxy (o re-exporta) los símbolos de una biblioteca diferente.
  • El comando LC_LOAD_UPWARD_DYLIB se usa cuando dos bibliotecas dependen entre sí (esto se llama una dependencia ascendente).

Sin embargo, hay 2 tipos de secuestro de dylib:

  • Bibliotecas vinculadas débiles faltantes: Esto significa que la aplicación intentará cargar una biblioteca que no existe configurada con LC_LOAD_WEAK_DYLIB. Luego, si un atacante coloca un dylib donde se espera que se cargue.
  • El hecho de que el enlace sea "débil" significa que la aplicación continuará ejecutándose incluso si la biblioteca no se encuentra.
  • El código relacionado con esto está en la función ImageLoaderMachO::doGetDependentLibraries de ImageLoaderMachO.cpp donde lib->required es solo false cuando LC_LOAD_WEAK_DYLIB es verdadero.
  • Encuentra bibliotecas vinculadas débiles en binarios con (tienes más adelante un ejemplo sobre cómo crear bibliotecas de secuestro):

otool -l </path/to/bin> | grep LC_LOAD_WEAK_DYLIB -A 5 cmd LC_LOAD_WEAK_DYLIB cmdsize 56 name /var/tmp/lib/libUtl.1.dylib (offset 24) time stamp 2 Wed Jun 21 12:23:31 1969 current version 1.0.0 compatibility version 1.0.0

- **Configurado con @rpath**: Los binarios Mach-O pueden tener los comandos **`LC_RPATH`** y **`LC_LOAD_DYLIB`**. Basado en los **valores** de esos comandos, las **bibliotecas** se cargarán desde **diferentes directorios**.
- **`LC_RPATH`** contiene las rutas de algunas carpetas utilizadas para cargar bibliotecas por el binario.
- **`LC_LOAD_DYLIB`** contiene la ruta a bibliotecas específicas para cargar. Estas rutas pueden contener **`@rpath`**, que será **reemplazado** por los valores en **`LC_RPATH`**. Si hay varias rutas en **`LC_RPATH`**, todas se utilizarán para buscar la biblioteca a cargar. Ejemplo:
- Si **`LC_LOAD_DYLIB`** contiene `@rpath/library.dylib` y **`LC_RPATH`** contiene `/application/app.app/Contents/Framework/v1/` y `/application/app.app/Contents/Framework/v2/`. Ambas carpetas se utilizarán para cargar `library.dylib`**.** Si la biblioteca no existe en `[...]/v1/` y el atacante podría colocarla allí para secuestrar la carga de la biblioteca en `[...]/v2/` ya que se sigue el orden de rutas en **`LC_LOAD_DYLIB`**.
- **Encuentra rutas y bibliotecas rpath** en binarios con: `otool -l </path/to/binary> | grep -E "LC_RPATH|LC_LOAD_DYLIB" -A 5`

> [!NOTE] > **`@executable_path`**: Es la **ruta** al directorio que contiene el **archivo ejecutable principal**.
>
> **`@loader_path`**: Es la **ruta** al **directorio** que contiene el **binario Mach-O** que contiene el comando de carga.
>
> - Cuando se usa en un ejecutable, **`@loader_path`** es efectivamente lo **mismo** que **`@executable_path`**.
> - Cuando se usa en un **dylib**, **`@loader_path`** da la **ruta** al **dylib**.

La forma de **escalar privilegios** abusando de esta funcionalidad sería en el raro caso de que una **aplicación** que se está ejecutando **por** **root** esté **buscando** alguna **biblioteca en alguna carpeta donde el atacante tiene permisos de escritura.**

<div class="mdbook-alerts mdbook-alerts-tip">
<p class="mdbook-alerts-title">
  <span class="mdbook-alerts-icon"></span>
  tip
</p>


Un buen **escáner** para encontrar **bibliotecas faltantes** en aplicaciones es [**Dylib Hijack Scanner**](https://objective-see.com/products/dhs.html) o una [**versión CLI**](https://github.com/pandazheng/DylibHijack).\
Un buen **informe con detalles técnicos** sobre esta técnica se puede encontrar [**aquí**](https://www.virusbulletin.com/virusbulletin/2015/03/dylib-hijacking-os-x).

</div>


**Ejemplo**

<a class="content_ref" href="macos-dyld-hijacking-and-dyld_insert_libraries.md"><span class="content_ref_label">macOS Dyld Hijacking & DYLD_INSERT_LIBRARIES</span></a>

## Secuestro de Dlopen

<div class="mdbook-alerts mdbook-alerts-caution">
<p class="mdbook-alerts-title">
  <span class="mdbook-alerts-icon"></span>
  caution
</p>


Recuerda que **las restricciones de Validación de Bibliotecas anteriores también se aplican** para realizar ataques de secuestro de Dlopen.

</div>


Desde **`man dlopen`**:

- Cuando la ruta **no contiene un carácter de barra** (es decir, es solo un nombre de hoja), **dlopen() realizará una búsqueda**. Si **`$DYLD_LIBRARY_PATH`** se estableció al inicio, dyld primero **mirará en ese directorio**. Luego, si el archivo macho que llama o el ejecutable principal especifican un **`LC_RPATH`**, entonces dyld **mirará en esos** directorios. A continuación, si el proceso es **sin restricciones**, dyld buscará en el **directorio de trabajo actual**. Por último, para binarios antiguos, dyld intentará algunas alternativas. Si **`$DYLD_FALLBACK_LIBRARY_PATH`** se estableció al inicio, dyld buscará en **esos directorios**, de lo contrario, dyld buscará en **`/usr/local/lib/`** (si el proceso es sin restricciones), y luego en **`/usr/lib/`** (esta información fue tomada de **`man dlopen`**).
1. `$DYLD_LIBRARY_PATH`
2. `LC_RPATH`
3. `CWD`(si no está restringido)
4. `$DYLD_FALLBACK_LIBRARY_PATH`
5. `/usr/local/lib/` (si no está restringido)
6. `/usr/lib/`

<div class="mdbook-alerts mdbook-alerts-caution">
<p class="mdbook-alerts-title">
  <span class="mdbook-alerts-icon"></span>
  caution
</p>


Si no hay barras en el nombre, habría 2 formas de hacer un secuestro:

- Si algún **`LC_RPATH`** es **escribible** (pero se verifica la firma, así que para esto también necesitas que el binario no esté restringido)
- Si el binario es **sin restricciones** y luego es posible cargar algo desde el CWD (o abusando de una de las variables de entorno mencionadas)

</div>


- Cuando la ruta **parece un marco** (por ejemplo, `/stuff/foo.framework/foo`), si **`$DYLD_FRAMEWORK_PATH`** se estableció al inicio, dyld primero buscará en ese directorio la **ruta parcial del marco** (por ejemplo, `foo.framework/foo`). Luego, dyld intentará la **ruta proporcionada tal cual** (usando el directorio de trabajo actual para rutas relativas). Por último, para binarios antiguos, dyld intentará algunas alternativas. Si **`$DYLD_FALLBACK_FRAMEWORK_PATH`** se estableció al inicio, dyld buscará en esos directorios. De lo contrario, buscará en **`/Library/Frameworks`** (en macOS si el proceso es sin restricciones), luego en **`/System/Library/Frameworks`**.
1. `$DYLD_FRAMEWORK_PATH`
2. ruta proporcionada (usando el directorio de trabajo actual para rutas relativas si no está restringido)
3. `$DYLD_FALLBACK_FRAMEWORK_PATH`
4. `/Library/Frameworks` (si no está restringido)
5. `/System/Library/Frameworks`

<div class="mdbook-alerts mdbook-alerts-caution">
<p class="mdbook-alerts-title">
  <span class="mdbook-alerts-icon"></span>
  caution
</p>


Si es una ruta de marco, la forma de secuestrarlo sería:

- Si el proceso es **sin restricciones**, abusando de la **ruta relativa desde CWD** las variables de entorno mencionadas (incluso si no se dice en la documentación si el proceso está restringido, las variables de entorno DYLD\_\* son eliminadas)

</div>


- Cuando la ruta **contiene una barra pero no es una ruta de marco** (es decir, una ruta completa o una ruta parcial a un dylib), dlopen() primero busca en (si está establecido) en **`$DYLD_LIBRARY_PATH`** (con la parte de hoja de la ruta). Luego, dyld **intenta la ruta proporcionada** (usando el directorio de trabajo actual para rutas relativas (pero solo para procesos sin restricciones)). Por último, para binarios más antiguos, dyld intentará alternativas. Si **`$DYLD_FALLBACK_LIBRARY_PATH`** se estableció al inicio, dyld buscará en esos directorios, de lo contrario, dyld buscará en **`/usr/local/lib/`** (si el proceso es sin restricciones), y luego en **`/usr/lib/`**.
1. `$DYLD_LIBRARY_PATH`
2. ruta proporcionada (usando el directorio de trabajo actual para rutas relativas si no está restringido)
3. `$DYLD_FALLBACK_LIBRARY_PATH`
4. `/usr/local/lib/` (si no está restringido)
5. `/usr/lib/`

<div class="mdbook-alerts mdbook-alerts-caution">
<p class="mdbook-alerts-title">
  <span class="mdbook-alerts-icon"></span>
  caution
</p>


Si hay barras en el nombre y no es un marco, la forma de secuestrarlo sería:

- Si el binario es **sin restricciones** y luego es posible cargar algo desde el CWD o `/usr/local/lib` (o abusando de una de las variables de entorno mencionadas)

</div>


<div class="mdbook-alerts mdbook-alerts-note">
<p class="mdbook-alerts-title">
  <span class="mdbook-alerts-icon"></span>
  note
</p>


Nota: No hay **archivos de configuración** para **controlar la búsqueda de dlopen**.

Nota: Si el ejecutable principal es un **binario set\[ug]id o firmado con derechos**, entonces **todas las variables de entorno son ignoradas**, y solo se puede usar una ruta completa ([ver restricciones de DYLD_INSERT_LIBRARIES](macos-dyld-hijacking-and-dyld_insert_libraries.md#check-dyld_insert_librery-restrictions) para más información detallada)

Nota: Las plataformas de Apple utilizan archivos "universales" para combinar bibliotecas de 32 bits y 64 bits. Esto significa que no hay **rutas de búsqueda separadas de 32 bits y 64 bits**.

Nota: En las plataformas de Apple, la mayoría de los dylibs del sistema están **combinados en la caché de dyld** y no existen en el disco. Por lo tanto, llamar a **`stat()`** para preflight si un dylib del sistema existe **no funcionará**. Sin embargo, **`dlopen_preflight()`** utiliza los mismos pasos que **`dlopen()`** para encontrar un archivo mach-o compatible.

</div>


**Verificar rutas**

Vamos a verificar todas las opciones con el siguiente código:

// gcc dlopentest.c -o dlopentest -Wl,-rpath,/tmp/test #include <dlfcn.h> #include <stdio.h>

int main(void) { void* handle;

fprintf("--- No slash ---\n"); handle = dlopen("just_name_dlopentest.dylib",1); if (!handle) { fprintf(stderr, "Error loading: %s\n\n\n", dlerror()); }

fprintf("--- Relative framework ---\n"); handle = dlopen("a/framework/rel_framework_dlopentest.dylib",1); if (!handle) { fprintf(stderr, "Error loading: %s\n\n\n", dlerror()); }

fprintf("--- Abs framework ---\n"); handle = dlopen("/a/abs/framework/abs_framework_dlopentest.dylib",1); if (!handle) { fprintf(stderr, "Error loading: %s\n\n\n", dlerror()); }

fprintf("--- Relative Path ---\n"); handle = dlopen("a/folder/rel_folder_dlopentest.dylib",1); if (!handle) { fprintf(stderr, "Error loading: %s\n\n\n", dlerror()); }

fprintf("--- Abs Path ---\n"); handle = dlopen("/a/abs/folder/abs_folder_dlopentest.dylib",1); if (!handle) { fprintf(stderr, "Error loading: %s\n\n\n", dlerror()); }

return 0; }

Si lo compilas y lo ejecutas, puedes ver **dónde se buscó cada biblioteca sin éxito**. También podrías **filtrar los registros del sistema de archivos**:

sudo fs_usage | grep "dlopentest"

## Secuestro de Ruta Relativa

Si un **binario/app privilegiado** (como un SUID o algún binario con privilegios poderosos) está **cargando una biblioteca de ruta relativa** (por ejemplo, usando `@executable_path` o `@loader_path`) y tiene **la Validación de Biblioteca deshabilitada**, podría ser posible mover el binario a una ubicación donde el atacante podría **modificar la biblioteca de ruta relativa cargada**, y abusar de ella para inyectar código en el proceso.

## Podar las variables de entorno `DYLD_*` y `LD_LIBRARY_PATH`

En el archivo `dyld-dyld-832.7.1/src/dyld2.cpp` es posible encontrar la función **`pruneEnvironmentVariables`**, que eliminará cualquier variable de entorno que **comience con `DYLD_`** y **`LD_LIBRARY_PATH=`**.

También establecerá en **null** específicamente las variables de entorno **`DYLD_FALLBACK_FRAMEWORK_PATH`** y **`DYLD_FALLBACK_LIBRARY_PATH`** para binarios **suid** y **sgid**.

Esta función se llama desde la función **`_main`** del mismo archivo si se dirige a OSX de esta manera:

#if TARGET_OS_OSX if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) { pruneEnvironmentVariables(envp, &apple);

y esos flags booleanos se establecen en el mismo archivo en el código:

#if TARGET_OS_OSX // support chrooting from old kernel bool isRestricted = false; bool libraryValidation = false; // any processes with setuid or setgid bit set or with __RESTRICT segment is restricted if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) { isRestricted = true; } bool usingSIP = (csr_check(CSR_ALLOW_TASK_FOR_PID) != 0); uint32_t flags; if ( csops(0, CS_OPS_STATUS, &flags, sizeof(flags)) != -1 ) { // On OS X CS_RESTRICT means the program was signed with entitlements if ( ((flags & CS_RESTRICT) == CS_RESTRICT) && usingSIP ) { isRestricted = true; } // Library Validation loosens searching but requires everything to be code signed if ( flags & CS_REQUIRE_LV ) { isRestricted = false; libraryValidation = true; } } gLinkContext.allowAtPaths = !isRestricted; gLinkContext.allowEnvVarsPrint = !isRestricted; gLinkContext.allowEnvVarsPath = !isRestricted; gLinkContext.allowEnvVarsSharedCache = !libraryValidation || !usingSIP; gLinkContext.allowClassicFallbackPaths = !isRestricted; gLinkContext.allowInsertFailures = false; gLinkContext.allowInterposing = true;

Lo que básicamente significa que si el binario es **suid** o **sgid**, o tiene un segmento **RESTRICT** en los encabezados o fue firmado con la bandera **CS_RESTRICT**, entonces **`!gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache`** es verdadero y las variables de entorno son eliminadas.

Tenga en cuenta que si CS_REQUIRE_LV es verdadero, entonces las variables no serán eliminadas, pero la validación de la biblioteca verificará que estén usando el mismo certificado que el binario original.

## Verificar Restricciones

### SUID & SGID

Make it owned by root and suid

sudo chown root hello sudo chmod +s hello

Insert the library

DYLD_INSERT_LIBRARIES=inject.dylib ./hello

Remove suid

sudo chmod -s hello

### Sección `__RESTRICT` con segmento `__restrict`

gcc -sectcreate __RESTRICT __restrict /dev/null hello.c -o hello-restrict DYLD_INSERT_LIBRARIES=inject.dylib ./hello-restrict

### Runtime endurecido

Crea un nuevo certificado en el llavero y úsalo para firmar el binario:

Apply runtime proetction

codesign -s --option=runtime ./hello DYLD_INSERT_LIBRARIES=inject.dylib ./hello #Library won't be injected

Apply library validation

codesign -f -s --option=library ./hello DYLD_INSERT_LIBRARIES=inject.dylib ./hello-signed #Will throw an error because signature of binary and library aren't signed by same cert (signs must be from a valid Apple-signed developer certificate)

Sign it

If the signature is from an unverified developer the injection will still work

If it's from a verified developer, it won't

codesign -f -s inject.dylib DYLD_INSERT_LIBRARIES=inject.dylib ./hello-signed

Apply CS_RESTRICT protection

codesign -f -s --option=restrict hello-signed DYLD_INSERT_LIBRARIES=inject.dylib ./hello-signed # Won't work