CreateRemoteThread para Linux

Mitsuha trae nuevas transmisiones WinAPI tiene una función CreateRemoteThread que le permite iniciar un nuevo hilo en el espacio de direcciones de otro proceso. Se puede usar para una variedad de inyecciones de DLL, tanto para fines malos (trucos en juegos, robo de contraseñas, etc.) como para corregir un error en un programa en ejecución sobre la marcha o agregar complementos a lugares donde no estaban proporcionado.


En general, esta función tiene una utilidad de aplicación dudosa, por lo tanto, no es sorprendente que Linux no tenga un análogo listo de CreateRemoteThread. Sin embargo, me preguntaba cómo se puede implementar. Estudiar el tema se convirtió en una buena aventura.


Hablaré en detalle sobre cómo, con la ayuda de la especificación ELF, algunos conocimientos de arquitectura x86_64 y llamadas al sistema Linux, escribir su propio pequeño depurador que pueda cargar y ejecutar código arbitrario en un proceso que ya se está ejecutando y funciona.


La comprensión del texto requerirá conocimientos básicos sobre la programación del sistema para Linux: el lenguaje C, la escritura y los programas de depuración, la comprensión del papel del código de máquina y la memoria en la computadora, el concepto de llamadas al sistema, la familiaridad con las bibliotecas principales y la lectura de la documentación.



Como resultado, pude "agregar" la capacidad de obtener una vista previa de las contraseñas en el Centro de control de Gnome:


demostración de inyección en el Centro de Control de Gnomos


Ideas principales


Si no hubiera una cláusula en los requisitos sobre la carga del código en un proceso que ya se está ejecutando, la solución sería extremadamente simple: LD_PRELOAD. Esta variable de entorno permite cargar una biblioteca arbitraria con la aplicación. En las bibliotecas compartidas, puede definir funciones de constructor que se ejecutan cuando se carga la biblioteca.


Juntos, LD_PRELOAD y los constructores permiten ejecutar código arbitrario en cualquier proceso utilizando un cargador dinámico. Esta es una característica relativamente conocida que se usa a menudo para la depuración. Por ejemplo, puede descargar su propia biblioteca con la aplicación, que define las funciones malloc () y free (), que podrían ayudar a detectar pérdidas de memoria.


Desafortunadamente, LD_PRELOAD solo funciona cuando comienza el proceso. No se puede usar para cargar una biblioteca en un proceso que ya se está ejecutando. Hay una función dlopen () para cargar bibliotecas mientras se ejecuta el proceso, pero, obviamente, el proceso en sí debe llamarlo para cargar los complementos.


Sobre ejecutables estáticos

LD_PRELOAD solo funciona con programas que usan el cargador dinámico. Si el programa se creó con el -static , entonces incluye todas las bibliotecas necesarias. En este caso, la resolución de dependencias en las bibliotecas se realiza en el momento de la compilación y el programa generalmente no está listo y no puede cargar dinámicamente las bibliotecas después del ensamblaje, en tiempo de ejecución.

En programas ensamblados estáticamente, puede incrustar código en tiempo de ejecución, pero esto debe hacerse de una manera ligeramente diferente. Y esto no es del todo seguro, ya que el programa puede no estar listo para ese giro.

En general, no existe una solución conveniente ya hecha, debe escribir su bicicleta. De lo contrario, no leerías este texto :)


Conceptualmente, para forzar el proceso de otra persona para ejecutar algún tipo de código, debe realizar las siguientes acciones:


  1. Obtenga control en el proceso de destino.
  2. Cargue el código en la memoria del proceso de destino.
  3. Prepare el código descargado para su ejecución en el proceso de destino.
  4. Organice la ejecución del código descargado por el proceso de destino.

Vamos ...


Conseguir control en el proceso


En primer lugar, debemos subordinar el proceso objetivo a nuestra voluntad. Después de todo, generalmente los procesos ejecutan solo su propio código, o el código de las bibliotecas cargadas, o los resultados de la compilación JIT. Pero ciertamente no es nuestro código.


Una opción es usar algún tipo de vulnerabilidad en el proceso que le permita tomar el control. Un ejemplo clásico de tutoriales: desbordamiento de búfer, que permite reescribir la dirección de retorno en la pila. Es divertido, a veces incluso funciona, pero no es adecuado para el caso general.


Usaremos otra forma honesta para obtener el control: depurar las llamadas del sistema . Los depuradores interactivos pueden detener perfectamente los procesos de terceros, evaluar expresiones y muchas otras cosas. Ellos pueden, nosotros podemos.


En Linux, la llamada principal del sistema de depuración es ptrace () . Le permite conectarse a procesos, examinar su estado y controlar el progreso de su ejecución. ptrace () está bastante bien documentado por sí solo, pero los detalles de su uso solo están claros en la práctica.


Cargando código en la memoria de proceso


En el caso de desbordamientos del búfer, la carga útil ( código de shell ) generalmente se incluye en los contenidos que desbordan el mismo búfer. Cuando se usa el depurador, el código necesario se puede escribir en la memoria del proceso directamente. En WinAPI hay una función especial WriteProcessMemory para esto. Linux para este propósito cumple con la forma UNIX: para cada proceso en el sistema hay un archivo / proc / $ pid / mem , que muestra la memoria de este proceso. Es posible escribir algo en la memoria de proceso utilizando la entrada-salida habitual.


Preparando código para ejecución


Simplemente escribir código en la memoria no es suficiente. Todavía necesita escribirse en la memoria ejecutable . En el caso de grabar a través de una vulnerabilidad, existen dificultades no triviales con esto, pero como podemos controlar completamente el proceso objetivo, no será un problema para nosotros encontrar o asignar la memoria "correcta" para nosotros mismos.


Otro punto importante de preparación es el propio código de shell. En él, probablemente querremos usar algunas funciones de las bibliotecas, como entrada-salida, primitivas gráficas, etc. Sin embargo, tenemos que escribir el código de máquina simple, que en sí mismo no tiene idea de las direcciones de todas estas funciones interesantes en las bibliotecas. ¿De dónde los sacaste?


Para simplificar la vida del sistema operativo y complicar la vida del código malicioso, las bibliotecas generalmente no usan direcciones fijas (y contienen el llamado código independiente de la posición ). Entonces las direcciones no se pueden adivinar.


Cuando el proceso comienza normalmente, el cargador que realiza las reubicaciones es responsable de determinar las direcciones exactas de las bibliotecas. Sin embargo, solo cumple una vez al comienzo. Si el proceso permite la carga dinámica de bibliotecas, entonces hay un cargador dinámico que puede hacer lo mismo mientras se ejecuta el proceso. Sin embargo, la dirección del cargador dinámico tampoco es fija.


En general, con las bibliotecas, hay cuatro opciones:


  • no use bibliotecas en absoluto, haga todo en llamadas de sistema limpio
  • poner copias de todas las bibliotecas necesarias en el código de shell
  • haz tú mismo el trabajo del cargador dinámico
  • encuentra un gestor de arranque dinámico y haz que cargue nuestras bibliotecas

Elegiremos este último, porque queremos las bibliotecas, y escribiremos nuestro cargador de arranque completo durante mucho tiempo. Este no es el método más reservado, ni el más interesante, sino el más simple, poderoso y confiable.


Transferencia de control a código


ptrace () le permite cambiar los registros del procesador, por lo que no debería haber problemas para transferir el control al código cargado y preparado: simplemente escriba la dirección de nuestro código en el registro% rip, ¡y listo! Sin embargo, en realidad, no todo es tan simple. Las dificultades están relacionadas con el hecho de que el proceso de depuración en realidad no ha desaparecido y también tiene algún tipo de código que se ha ejecutado y continuará ejecutándose.


Bosquejo de la solución


Total, implementaremos nuestro flujo en un proceso de terceros de la siguiente manera:


  1. Estamos conectados al proceso de destino para la depuración.
  2. Encontramos las bibliotecas necesarias en la memoria:
    • libdl - para cargar una nueva biblioteca
    • libpthread - para comenzar un nuevo hilo
  3. Encontramos las funciones necesarias en las bibliotecas:
    • libdl: dlopen (), dlsym ()
    • libpthread: pthread_create (), pthread_detach ()
  4. Introducimos el código de shell en la memoria del proceso de destino:


     void shellcode(void) { void *payload = dlopen("/path/to/payload.so", RTLD_LAZY); void *entry = dlsym(payload, "entry_point"); pthread_t thread; pthread_create(&thread, NULL, entry, NULL); pthread_detach(thread); } 

  5. Damos el código de shell para que se cumpla.

Como resultado, las bibliotecas harán lo correcto por nosotros: cargarán nuestra biblioteca con el código que necesitamos en la memoria y comenzarán un nuevo hilo ejecutando este código.


Limitaciones


El enfoque descrito anteriormente impone ciertas limitaciones:


  • El gestor de arranque debe tener privilegios suficientes para depurar el proceso de destino.
  • El proceso debe usar libdl (listo para la carga dinámica de módulos).
  • El proceso debe usar libpthread (listo para subprocesamiento múltiple).
  • Las aplicaciones estáticas no son compatibles.

Además, personalmente soy demasiado vago para molestarme con el soporte de todas las arquitecturas, por lo que nos limitaremos a x86_64. (Incluso un x86 de 32 bits sería más complicado).


Como puede ver, todo esto pone fin al uso encubierto con objetivos maliciosos. Sin embargo, la tarea aún conserva el interés de la investigación e incluso deja una oportunidad débil para uso industrial.


Digresión: sobre el uso de libdl y libpthread


Un lector-lector experimentado puede preguntarse: ¿por qué requerir libdl si las funciones internas __libc_dlopen_mode () y __libc_dlsym () ya están integradas en glibc, y libdl es solo una envoltura sobre ellas? Del mismo modo, ¿por qué requerir libpthread si se puede crear fácilmente un nuevo hilo usando la llamada al sistema clone ()?


De hecho, en Internet hay lejos de un ejemplo de cómo se usan:



Incluso se mencionan en la literatura popular de hackers:


  • Aprendizaje del análisis binario de Linux
  • El arte de la memoria forense

Entonces, ¿por qué no? Bueno, al menos porque no estamos escribiendo código malicioso donde una solución es adecuada que omite el 90% de las comprobaciones, ocupa 20 veces menos espacio, pero también funciona en el 80% de los casos. Además, quería probar todo con mis propias manos.


De hecho, libdl no es necesario cargar la biblioteca en el caso de glibc. Su uso por el proceso indica que está claramente listo para la carga dinámica de código. A pesar de esto, en principio, puede negarse a usar libdl (dado que también tendremos que buscar glibc más adelante).


¿Por qué dlopen () dentro de glibc?

Esta es una pregunta interesante a su manera. Respuesta corta: detalles de implementación.

El punto es el conmutador de servicio de nombres (NSS), una de las partes de glibc que proporciona traducción de varios nombres: nombres de máquinas, protocolos, usuarios, servidores de correo, etc. Es ella quien es responsable de funciones como getaddrinfo () para obtener direcciones IP mediante nombre de dominio y getpwuid () para obtener información sobre el usuario por su identificador numérico.

NSS tiene una arquitectura modular y los módulos se cargan dinámicamente. En realidad, para esto, glibc también requería mecanismos para cargar bibliotecas dinámicamente. Es por eso que cuando intenta utilizar getaddrinfo () en una aplicación ensamblada estáticamente, el enlazador imprime una advertencia "incomprensible":
 /tmp/build/socket.o: en la función `Socket :: bind ':
 socket.o :(. text + 0x374): advertencia: Uso de 'getaddrinfo' en enlaces estáticos
 las aplicaciones requieren en tiempo de ejecución las bibliotecas compartidas de la versión glibc
 utilizado para vincular

En cuanto a los subprocesos, un subproceso generalmente no es solo una pila y un código ejecutable, sino también datos globales almacenados en el almacenamiento local de subprocesos (TLS). La inicialización correcta de un nuevo subproceso requiere la operación coordinada del núcleo del sistema operativo, un cargador de código binario y un tiempo de ejecución del lenguaje de programación. Por lo tanto, una simple llamada a clone () es suficiente para crear una secuencia que pueda escribir en el archivo "Hello world!", Pero esto puede no funcionar para un código más complejo que necesita acceso a TLS y otras cosas interesantes ocultas a los ojos del programador de la aplicación.


Otro punto relacionado con el subprocesamiento múltiple son los procesos de subproceso único. ¿Qué sucede si creamos un nuevo hilo en un proceso que no fue concebido como multiproceso? Correcto, comportamiento vago. De hecho, en el proceso no hay sincronización de trabajo entre hilos, lo que tarde o temprano conducirá a la corrupción de datos. Si exigimos que la aplicación use libpthread, entonces podemos estar seguros de que está lista para funcionar en un entorno de subprocesos múltiples (al menos debería estar lista).


Paso 1. Conexión al proceso


Primero, necesitamos conectarnos al proceso de destino para la depuración, y luego, desconectarnos de nuevo. Aquí es donde entra la llamada al sistema ptrace ().


Primer contacto con ptrace ()


En la documentación de ptrace () puede encontrar casi toda la información necesaria:


   Adjuntar y separar
        Se puede adjuntar un hilo al rastreador mediante la llamada

            ptrace (PTRACE_ATTACH, pid, 0, 0);

        o

            ptrace (PTRACE_SEIZE, pid, 0, PTRACE_O_flags);

        PTRACE_ATTACH envía SIGSTOP a este hilo.  Si el trazador quiere esto
        SIGSTOP para no tener efecto, necesita suprimirlo.  Tenga en cuenta que si
        otras señales se envían simultáneamente a este hilo durante la conexión, el
        el rastreador puede ver al traza ingresar señal-entrega-parada con otra señal
        nal (s) primero!  La práctica habitual es reinyectar estas señales hasta
        Se ve SIGSTOP, luego suprima la inyección de SIGSTOP.  El error de diseño
        aquí es que un archivo adjunto ptrace y un SIGSTOP entregado simultáneamente
        carrera y el SIGSTOP concurrente pueden perderse.

Entonces, el primer paso es usar PTRACE_ATTACH:


 int ptrace_attach(pid_t pid) { /*     */ if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) return -1; /*    */ if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; return 0; } 

Después de ptrace (), el proceso de destino no está listo para la depuración. Nos conectamos a él, pero para un estudio interactivo del estado del proceso, debe detenerse. ptrace () envía una señal SIGSTOP al proceso, pero aún debemos esperar hasta que el proceso realmente se detenga.


Para esperar, use la llamada al sistema waitpid (). Al mismo tiempo, vale la pena señalar varios casos límite interesantes. En primer lugar, el proceso puede simplemente terminar o morir sin haber recibido SIGSTOP. En este caso, no podemos hacer nada. En segundo lugar, alguna otra señal puede enviarse previamente al proceso. En este caso, debemos dejar que el proceso lo procese (usando PTRACE_CONT), y a nosotros mismos, continuar esperando más para nuestro SIGSTOP:


 static int wait_for_process_stop(pid_t pid, int expected_signal) { for (;;) { int status = 0; /* ,    -  */ if (waitpid(pid, &status, 0) < 0) return -1; /*      —   */ if (WIFSIGNALED(status) || WIFEXITED(status)) return -1; /*   ,     */ if (WIFSTOPPED(status)) { /* *  WSTOPSIG()   , *   ptrace()   *     . */ int stop_signal = status >> 8; /*    ,    */ if (stop_signal == expected_signal) break; /*        */ if (ptrace(PTRACE_CONT, pid, 0, stop_signal) < 0) return -1; continue; } /*   —   */ return -1; } return 0; } 

Desconexión del proceso


Detener el proceso de depuración es mucho más simple: solo use PTRACE_DETACH:


 int ptrace_detach(pid_t pid) { if (ptrace(PTRACE_DETACH, pid, 0, 0) < 0) return -1; return 0; } 

Estrictamente hablando, deshabilitar explícitamente el depurador no siempre es necesario. Cuando finaliza el proceso del depurador, se desconecta automáticamente de todos los procesos depurados, y los procesos se reanudan si fueron detenidos por ptrace (). Sin embargo, si el depurador detuvo explícitamente el proceso de depuración utilizando la señal SIGSTOP sin utilizar ptrace (), no se activará sin la señal SIGCONT o PTRACE_DETACH correspondiente. Por lo tanto, es mejor desconectarse de los procesos culturalmente.


Configuración de Ptrace_scope


El depurador tiene control total sobre el proceso que se está depurando. Si alguien pudiera depurar algo, ¿cuál sería la extensión del código malicioso? Es obvio que la depuración interactiva es una actividad bastante específica, generalmente necesaria solo para desarrolladores. Durante el funcionamiento normal del sistema, la mayoría de las veces no es necesario depurar procesos.


Por estas razones, por razones de seguridad, los sistemas generalmente deshabilitan la capacidad de depurar cualquier proceso por defecto. El módulo de seguridad de Yama es responsable de esto, administrado a través del archivo / proc / sys / kernel / yama / ptrace_scope. Proporciona cuatro comportamientos:


  • 0: el usuario puede depurar cualquier proceso que haya iniciado
  • 1 - modo predeterminado, solo se pueden depurar los procesos iniciados por el depurador
  • 2: solo un administrador del sistema raíz puede depurar procesos
  • 3 - la depuración está prohibida para todos, el modo no se apaga hasta que el sistema se reinicia

Obviamente, para nuestros propósitos, deberá poder depurar los procesos que se iniciaron antes de nuestro depurador, por lo que para los experimentos deberá cambiar el sistema al modo de desarrollo escribiendo 0 en un archivo especial ptrace_scope (que requiere derechos de administrador):


 $ sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope' 

o ejecute el depurador como administrador:


 $ sudo ./inject-thread ... 

Resultados del primer paso


Como resultado, en el primer paso, podemos conectarnos al proceso de destino como depurador y luego desconectarnos de él.


El proceso de destino se detendrá y podemos asegurarnos de que el sistema operativo realmente nos vea como un depurador:


 $ sudo ./inject-thread --target $(pgrep docker) $ cat /proc/$(pgrep docker)/status | head Name: docker State: t (tracing stop) <---    Tgid: 31330 Ngid: 0 Pid: 31330 PPid: 1 TracerPid: 2789 <--- PID   Uid: 0 0 0 0 Gid: 0 0 0 0 FDSize: 64 $ ps a | grep [2]789 2789 pts/5 S+ 0:00 ./inject-thread --target 31330 

Paso 2. Buscar bibliotecas en memoria


El siguiente paso es más simple: necesita encontrar en la memoria del proceso de destino la biblioteca con las funciones que necesitamos. Pero hay mucha memoria, ¿por dónde empezar a buscar y qué exactamente?


Archivo / proc / $ pid / maps


Un archivo especial nos ayudará con esto, a través del cual el núcleo informa sobre qué y dónde se encuentra el proceso en la memoria. Como sabe , en el directorio / proc para cada proceso hay un subdirectorio. Y hay un archivo que describe la tarjeta de memoria de proceso:


 $ cat / proc / self / maps
 00400000-0040c000 r-xp 00000000 fe: 01 1044592 / bin / cat
 0060b000-0060c000 r - p 0000b000 fe: 01 1044592 / bin / cat
 0060c000-0060d000 rw-p 0000c000 fe: 01 1044592 / bin / cat
 013d5000-013f6000 rw-p 00000000 00:00 0 [montón]
 7f9920bd1000-7f9920d72000 r-xp 00000000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7f9920d72000-7f9920f72000 --- p 001a1000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7f9920f72000-7f9920f76000 r - p 001a1000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7f9920f76000-7f9920f78000 rw-p 001a5000 fe: 01 920019 /lib/x86_64-linux-gnu/libc-2.19.so
 7fc3f8381000-7fc3f8385000 rw-p 00000000 00:00 0
 7fc3f8385000-7fc3f83a6000 r-xp 00000000 fe: 01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
 7fc3f83ec000-7fc3f840e000 rw-p 00000000 00:00 0
 7fc3f840e000-7fc3f8597000 r - p 00000000 fe: 01 657286 / usr / lib / locale / locale-archive
 7fc3f8597000-7fc3f859a000 rw-p 00000000 00:00 0
 7fc3f85a3000-7fc3f85a5000 rw-p 00000000 00:00 0
 7fc3f85a5000-7fc3f85a6000 r - p 00020000 fe: 01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
 7fc3f85a6000-7fc3f85a7000 rw-p 00021000 fe: 01 920012 /lib/x86_64-linux-gnu/ld-2.19.so
 7fc3f85a7000-7fc3f85a8000 rw-p 00000000 00:00 0
 7ffdb6f0e000-7ffdb6f2f000 rw-p 00000000 00:00 0 [pila]
 7ffdb6f7f000-7ffdb6f81000 r-xp 00000000 00:00 0 [vdso]
 7ffdb6f81000-7ffdb6f83000 r - p 00000000 00:00 0 [vvar]
 ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

El núcleo del sistema operativo genera el contenido de este archivo sobre la marcha a partir de estructuras internas que describen las regiones de memoria del proceso que nos interesa y contiene la siguiente información:


  • rango de direcciones asignado a la región
  • derechos de acceso a la región
    • r/- : leer
    • w/- : escribir
    • x/- : ejecución
    • p/s : compartir memoria con otros procesos
  • desplazamiento de archivo (si lo hay)
  • código del dispositivo donde se encuentra el archivo mostrado
  • número de inodo de archivo (si lo hay)
  • ruta al archivo mostrado (si lo hay)

Algunas regiones de la memoria se asignan a archivos: cuando un proceso lee dicha memoria, en realidad lee los datos de los archivos correspondientes en un desplazamiento específico. Si puede escribir en una región, entonces los cambios en la memoria pueden ser visibles solo para el proceso en sí (mecanismo de copia en escritura , el modo p es privado) o sincronizados con el disco ( s modo s es compartido).


Otras regiones son anónimas : esta memoria no corresponde a ningún archivo. El sistema operativo simplemente le da al proceso una pieza de memoria física que utiliza. Dichas regiones se utilizan, por ejemplo, para la memoria de proceso "normal": pila y montón. Las regiones anónimas pueden ser personales para un proceso o compartidas entre varios procesos (mecanismo de memoria compartida ).


Además, hay varias regiones especiales en la memoria marcadas con los seudónimos [vdso] y [vsyscall]. Se utilizan para optimizar algunas llamadas al sistema.


Estamos interesados ​​en las regiones donde se muestran los contenidos de los archivos de la biblioteca. Si leemos la tarjeta de memoria y filtramos las entradas por el nombre del archivo que se muestra, encontraremos todas las direcciones ocupadas por las bibliotecas que necesitamos. El formato de la tarjeta de memoria es especialmente conveniente para el procesamiento del programa y se entiende fácilmente utilizando las funciones de la familia scanf ():


 static bool read_proc_line(const char *line, const char *library, struct memory_region *region) { unsigned long vaddr_low = 0; unsigned long vaddr_high = 0; char read = 0; char write = 0; char execute = 0; int path_offset = 0; /*    /proc/$pid/maps */ sscanf(line, "%lx-%lx %c%c%c%*c %*lx %*x:%*x %*d %n", &vaddr_low, &vaddr_high, &read, &write, &execute, &path_offset); /* ,       */ if (!strstr(line + path_offset, library)) return false; /*           */ if (region) { region->vaddr_low = vaddr_low; region->vaddr_high = vaddr_high; region->readable = (read == 'r'); region->writeable = (write == 'w'); region->executable = (execute == 'x'); region->content = NULL; } return true; } 


, libc-2.19.so, :


agujero en libc-2.19.so


2 - ? 51? ? ?


, , .


, , . , , , (, , ).


, ( 4 ). , .


, . — — . 2 — , ( x86_64 4 , 2 , 1 ). .



, :


  • libdl: dlopen() dlsym()
  • libpthread: pthread_create() pthread_detach()

, , . Linux ( address space layout randomization , ASLR). (- , ), — - .


, , , /proc/$pid/maps. , .


3. ELF-


, , , .


:


 $ nm -D /lib/x86_64-linux-gnu/libdl-2.19.so | grep dlopen 0000000000001090 T dlopen 

nm . .


- , nm , . , dlsym().



— ELF-, . procfs. UNIX way, /proc/$pid/mem , — ( /proc/$pid/maps).


Linux mmap(), ( , ). :


 static int map_region(pid_t pid, struct memory_region *region) { size_t length = region->vaddr_high - region->vaddr_low; off_t offset = region->vaddr_low; char path[32] = {0}; snprintf(path, sizeof(path), "/proc/%d/mem", pid); /*     */ int fd = open(path, O_RDONLY); if (fd < 0) goto error; /*      */ void *buffer = malloc(length); if (!buffer) goto error_close_file; /*   */ if (read_region(fd, offset, buffer, length) < 0) goto error_free_buffer; region->content = buffer; close(fd); return 0; error_free_buffer: free(buffer); error_close_file: close(fd); error: return -1; } static int read_region(int fd, off_t offset, void *buffer, size_t length) { /*      */ if (lseek(fd, offset, SEEK_SET) < 0) return -1; size_t remaining = length; char *ptr = buffer; /* *     .   , *      ,  . */ while (remaining > 0) { ssize_t count = read(fd, ptr, remaining); if (count < 0) return -1; remaining -= count; ptr += count; } return 0; } 

ELF- . , -, , -, .


ELF


ELF — Linux. , , .


ELF . ELF . — , . , — . ELF-.


, libdl-2.19.so :


secciones y segmentos libdl-2.19.so


( readelf --headers .)


, , (29 9). — , , . ELF — , . Linux, , LOAD, ( ).


ELF- , . , .


, . «» . .bss, , ( ).


, ELF — , . ...


?


() . , dlsym(), . - .


ELF (. 2-10). , .dynamic , DYNAMIC . .dynamic , :


  • .dynsym — ;
  • .dynstr — ;
  • .hash — -, .

, , ELF:


Búsqueda de segmento DINÁMICO


ELF, (1), (2), (3), (4) , .


ELF →


() ELF <elf.h>, , , . , ELF — . 32- 64- , , , . x86_64, ELF .


ELF- ( Elf64_Ehdr ). ( program headers ), e_phoff e_phnum :


Encabezado ELF


— , , ELF- — , , , , .


e_phoff, , . e_phnum e_phentsize .


( ), ELF — 64 .


→ DYNAMIC


. — Elf64_Phdr ( 64- ELF-), . PT_DYNAMIC p_type :


Tabla de segmentos ELF


:


  • p_vaddr — , ;
  • p_memsz — .

.dynamic 0x2D88 ( ). DYNAMIC — 0x202D88. 0x210 (8448) . .


DYNAMIC → .dynsym, .dynstr, .hash


.dynamic, DYNAMIC, . Elf64_Dyn , :


Etiquetas de sección DINÁMICAS


8 d_val d_ptr , 8- d_tag , , . :


  • DT_HASH (4) — .hash ( d_ptr)
  • DT_STRTAB (5) — .dynstr ( d_ptr)
  • DT_SYMTAB (6) — .dynsym ( d_ptr)
  • DT_STRSZ (10) — .dynstr ( d_val)
  • DT_NULL (0) —

. .dynamic : , , , .


, DYNAMIC , . , , - , .


.dynamic , . -, .dynstr , ? .



. , .dynsym , . ( «» .symtab, , , . .)



Elf64_Sym , ELF — , , , . dlopen :


Tabla de caracteres ELF


:


  • st_name — ,
  • st_info — ( )
  • st_value

( , nm , dlopen() .text, 0x1090 .)


, .



— - , . ( ). .dynstr , libdl-2.19.so :


Mesa de fila ELF


, ( «dlopen», 0xA5) , . .


-


.hash - , . - — — ELF-, . , .dynsym, , . ( ) - .


- <elf.h>, (. 2-19). - , :


tabla hash ELF


donde


  • nbuckets — buckets
  • nchains — chains ( )
  • buckets —
  • chains —

- :


  1. h .
  2. i buckets[h % nbuckets] , .
  3. ( ) , .
  4. chains[i % nchains] .
  5. 3—4 , .

-, ELF:


 static uint32_t elf_hash(const char *name) { uint32_t h = 0; uint32_t g; while (*name) { h = (h << 4) + *name++; g = h & 0xF0000000; if (g) h ^= g >> 24; h &= ~g; } return h; } 

, "dlopen" - 112420542 :


buscar un personaje en una biblioteca


libdl — , 39 , . - .



, :


  • dlopen() dlsym() libdl
  • pthread_create() pthread_detach() libpthread

, .


. . , .


ELF- . , ( ). , . , , . .


4. -


, , - , : , . - .


-


, -:


 void shellcode(void) { void *payload = dlopen("/path/to/payload.so", RTLD_LAZY); void (*entry)(void) = dlsym(payload, "entry_point"); pthread_t thread; pthread_create(&thread, NULL, entry, NULL); pthread_detach(thread); } 

?


, — . , , , - — - ! .


— - . , , : .


 /* *      .rodata:   * .         , *        . */ .section .rodata /* *   .       . *      -:    ,  *  ,       . */ .global shellcode_start .global shellcode_address_dlopen .global shellcode_address_dlsym .global shellcode_address_pthread_create .global shellcode_address_pthread_detach .global shellcode_address_payload .global shellcode_address_entry .global shellcode_end /* *   dlopen().     #include <dlfcn.h>, *       . */ .set RTLD_LAZY, 1 .align 8 shellcode_start: /* * void *payload = dlopen(shellcode_address_payload, RTLD_LAZY); * *        x86_64: * * -     %rdi, %rsi, %rdx, %rcx * -     %rax * -      * *         . * *       %rax,    *     . */ lea shellcode_address_payload(%rip),%rdi mov $RTLD_LAZY,%rsi mov shellcode_address_dlopen(%rip),%rax callq *%rax /* * void (*entry)(void) = dlsym(payload, shellcode_address_entry); */ mov %rax,%rdi lea shellcode_address_entry(%rip),%rsi mov shellcode_address_dlsym(%rip),%rax callq *%rax /* * pthread_t thread; * pthread_create(&thread, NULL, entry, NULL); * *            * ,     pthread_create(). */ sub $8,%rsp mov %rsp,%rdi xor %rsi,%rsi mov %rax,%rdx xor %rcx,%rcx mov shellcode_address_pthread_create(%rip),%rax callq *%rax /* * pthread_detach(thread); * *    ,   ,  *     . */ mov (%rsp),%rdi add $8,%rsp mov shellcode_address_pthread_detach(%rip),%rax callq *%rax /* *   - —    ,     *      ret.    *     ,  *      . */ int $3 /* *       ,   *   ,    - *     .   “  *  ” (global offset table, GOT),   *           . */ .align 8 shellcode_address_dlopen: .space 8 shellcode_address_dlsym: .space 8 shellcode_address_pthread_create: .space 8 shellcode_address_pthread_detach: .space 8 shellcode_address_payload: .space 256 shellcode_address_entry: .space 256 /* *  - . */ shellcode_end: .end 

, . :


 $ as -o shellcode.o shellcode.S 

, , , . : (procedure linkage table, PLT), .


- , (, ) . - .


-


- . , , , . ?


-


, . , . , . , .


(- ), : , , . , , JIT- , . ?



:


  • - ,
  • - ,

, . -, - , . -, . -, , - -, .


, . . x86_64 int $3 — 0xCC — . ptrace() PTRACE_POKETEXT — , 8 , . , , .


, , , : . - , .


?


, ! malloc()!


. , -, . . , mmap():


 void inject_shellcode(const void *shellcode_src, size_t shellcode_size) { void *shellcode_dst = mmap(NULL, shellcode_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); copy_shellcode(shellcode_dst, shellcode_src, shellcode_size); } 

, ptrace() , .



, ? , . Linux x86_64 :


  • %rax
  • — — %rsi, %rdi, %rdx, %r10, %r8, %r9
  • SYSCALL,
  • %rax

ptrace() PTRACE_GETREGS PTRACE_SETREGS. , . - SYSCALL.


: , %rip. , , SYSCALL.


SYSCALL


SYSCALL? , . - , - . — libc. , , , :


 unsigned long find_syscall_instruction(struct library *library) { for (size_t i = 0; i < library->region_count; i++) { struct memory_region *region = &library->regions[i]; if (!(region->readable && region->executable)) continue; const uint8_t *region_data = region->content; size_t region_size = region->vaddr_high - region->vaddr_low; if (region_size < 2) continue; /* * 0F 05 syscall */ for (size_t offset = 0; offset < region_size - 1; offset++) { if (region_data[offset + 0] == 0x0F && region_data[offset + 1] == 0x05) { return region->vaddr_low + offset; } } } return 0; } 

, /proc/$pid/maps . x86_64 , - . , 0x0F 0x05. , , ARM, 0xDF 0x00 ( SVC #0), .


PTRACE_{GET,SET}REGS


:


 int get_registers(pid_t pid, struct user_regs_struct *registers) { int err = 0; if (ptrace(PTRACE_GETREGS, pid, registers, registers) < 0) err = -errno; return err; } 

struct user_regs_struct , <sys/user.h>. . . , varargs :


 static int set_regs_for_syscall(struct user_regs_struct *registers, unsigned long syscall_insn_vaddr, long syscall_number, int args_count, va_list args) { registers->rip = syscall_insn_vaddr; registers->rax = syscall_number; for (int i = 0; i < args_count; i++) { switch (i) { case 0: registers->rdi = va_arg(args, long); break; case 1: registers->rsi = va_arg(args, long); break; case 2: registers->rdx = va_arg(args, long); break; case 3: registers->r10 = va_arg(args, long); break; case 4: registers->r8 = va_arg(args, long); break; case 5: registers->r9 = va_arg(args, long); break; default: return -E2BIG; } } return 0; } static long perform_syscall(pid_t pid, unsigned long syscall_insn_vaddr, long syscall_number, int args_count, ...) { struct user_regs_struct old_registers; struct user_regs_struct new_registers; /* *    ,   *      . */ get_registers(pid, &old_registers); /* *      ,   * ,     . */ new_registers = old_registers; va_list args; va_start(args, args_count); set_regs_for_syscall(&new_registers, syscall_insn_vaddr, syscall_number, args_count, args); va_end(args); set_registers(pid, &new_registers); /* *    ,    *   ,    * (  ),    . *     . */ wait_for_syscall_completion(pid); /* *       *    . *        . */ get_registers(pid, &new_registers); long result = new_registers.rax; set_registers(pid, &old_registers); return result; } 

PTRACE_SYSCALL


: , ?


PTRACE_SYSCALL. PTRACE_CONT, . , - : , .


PTRACE_SYSCALL SIGTRAP : ( ) ( ). , ptrace() , , .


, SIGTRAP:


 static int wait_for_syscall_enter_exit_stop(pid_t pid) { if (ptrace(PTRACE_SYSCALL, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGTRAP) < 0) return -1; return 0; } void wait_for_syscall_completion(pid_t pid) { wait_for_syscall_enter_exit_stop(pid); wait_for_syscall_enter_exit_stop(pid); } 

— , — (wait_for_process_stop() ). . , .


PTRACE_O_TRACESYSGOOD


, PTRACE_SYSCALL : , , - . , SIGTRAP ( ).


SIGTRAP . PTRACE_O_TRACESYSGOOD, :


  • SIGTRAP — -
  • SIGTRAP | 0x80 —

  int ptrace_attach(pid_t pid) { if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; + /*     */ + unsigned long options = PTRACE_O_TRACESYSGOOD; + if (ptrace(PTRACE_SETOPTIONS, pid, 0, options) < 0) + return -1; return 0; } static int wait_for_syscall_enter_exit_stop(pid_t pid) { if (ptrace(PTRACE_SYSCALL, pid, 0, 0) < 0) return -1; - if (wait_for_process_stop(pid, SIGTRAP) < 0) + if (wait_for_process_stop(pid, SIGTRAP | 0x80) < 0) return -1; return 0; } 

-


- :


 void write_shellcode(void) { char shellcode_text[SHELLCODE_TEXT_SIZE]; size_t shellcode_size = shellcode_end - shellcode_start; /*   ,  ,  . . */ prepare_shellcode(shellcode_text, shellcode_size); /*   -   */ write_remote_memory(target, shellcode_text_vaddr, shellcode_text, shellcode_size); } 

- : dlopen(), .


 static inline void copy_shellcode(char *shellcode_text, const char *shellcode_addr, const void *data, size_t length) { ptrdiff_t offset = shellcode_addr - shellcode_start; memcpy(shellcode_text + offset, data, length); } static void prepare_shellcode(char *shellcode_text, size_t shellcode_size) { copy_shellcode(shellcode_text, shellcode_start, shellcode_start, shellcode_size); copy_shellcode(shellcode_text, shellcode_address_dlopen, &dlopen_vaddr, sizeof(dlopen_vaddr)); copy_shellcode(shellcode_text, shellcode_address_dlsym, &dlsym_vaddr, sizeof(dlsym_vaddr)); copy_shellcode(shellcode_text, shellcode_address_pthread_create, &pthread_create_vaddr, sizeof(pthread_create_vaddr)); copy_shellcode(shellcode_text, shellcode_address_pthread_detach, &pthread_detach_vaddr, sizeof(pthread_detach_vaddr)); copy_shellcode(shellcode_text, shellcode_address_payload, payload, sizeof(payload)); copy_shellcode(shellcode_text, shellcode_address_entry, entry, sizeof(entry)); } 

, , -:


 extern const char shellcode_start[]; extern const char shellcode_address_dlopen[]; extern const char shellcode_address_dlsym[]; extern const char shellcode_address_pthread_create[]; extern const char shellcode_address_pthread_detach[]; extern const char shellcode_address_payload[]; extern const char shellcode_address_entry[]; extern const char shellcode_end[]; 

, .


- . /proc/$pid/mem, :


 int write_remote_memory(pid_t pid, unsigned long vaddr, const void *data, size_t size) { char path[32] = {0}; snprintf(path, sizeof(path), "/proc/%d/mem", pid); /*       */ int fd = open(path, O_WRONLY); if (fd < 0) return -1; /*     */ if (lseek(fd, vaddr, SEEK_SET) < 0) { close(fd); return -1; } /*    */ int err = do_write_remote_memory(fd, data, size); close(fd); return err; } static int do_write_remote_memory(int fd, const void *data, size_t size) { size_t left = size; /* *    ,  ,     *   ,       *      . */ while (left > 0) { ssize_t wrote = write(fd, data, left); if (wrote < 0) return -1; data += wrote; left -= wrote; } return 0; } 


, - — « » . . - , .


5.


- . , : %rip -, PTRACE_SETREGS, PTRACE_CONT . .


, , . -? ?



, . , « » . , . :


  • (async-signal-safe)

— . dlopen() pthread_create() . - dlopen(), dlopen() ?


-, , , . , pthread_create() . , ( ). clone().


pthread_create()?

, - , ?

: clone().

, (libc) (pthread). clone() (thread control block, TCB) (thread-local storage, TLS), , . . pthread_create() , .

«», clone() libc pthread. , .


clone() :


  • ?
  • ?
  • -?


: -?


, - : , , , .


. , , . ? : exit(). , .


. exit() -:


 +.set __NR_exit, 60 .set RTLD_LAZY, 1 @@ - /* - *  . - */ - int $3 + /* + * exit(0); + */ + xor %rdi,%rdi + mov $__NR_exit,%rax + syscall 

: exit() — exit() . exit() , exit() — . Linux exit_group().



. . , , PROT_EXEC:


 shellcode_stack_vaddr = remote_mmap(target, syscall_vaddr, 0, SHELLCODE_STACK_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN, -1, 0); 

, Linux x86_64 — «» , . mmap() , clone() . , mmap() MAP_GROWSDOWN, , .


PTRACE_O_TRACECLONE


. , - . waitpid(), : , .


— PTRACE_O_TRACECLONE. . , . , , , . , PTRACE_ATTACH , .


-, :


 - unsigned long options = PTRACE_O_TRACESYSGOOD; + unsigned long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACECLONE; if (ptrace(PTRACE_SETOPTIONS, pid, 0, options) < 0) return -1; 

-, clone(), PTRACE_EVENT_CLONE, , PTRACE_SYSCALL. :


 -void wait_for_syscall_completion(pid_t pid) +void wait_for_syscall_completion(pid_t pid, long syscall) { wait_for_syscall_enter_exit_stop(pid); + + /*  clone()   PTRACE_EVENT_CLONE */ + if (syscall == __NR_clone) + wait_for_clone_event(pid); wait_for_syscall_enter_exit_stop(pid); } 

:


 static int wait_for_clone_event(pid_t pid) { if (ptrace(PTRACE_CONT, pid, 0, 0) < 0) return -1; int event = SIGTRAP | (PTRACE_EVENT_CLONE << 8); if (wait_for_process_stop(pid, event) < 0) return -1; return 0; } 

clone() PID , . :


 void clear_ptrace_options(pid_t pid) { ptrace(PTRACE_SETOPTIONS, pid, 0, 0); } 

, clone() ptrace(), PTRACE_O_TRACECLONE. , , - .



, - . clone() :


 static int spawn_shell_thread() { shell_tid = remote_clone(target, syscall_ret_vaddr, CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_SIGHAND | CLONE_SYSVSEM | CLONE_THREAD | CLONE_VM, /*   **  */ shellcode_stack_vaddr + SHELLCODE_STACK_SIZE); if (!shell_tid) return -1; return 0; } 

clone() : , , , . , .


CLONE_FILES, CLONE_FS, CLONE_IO, CLONE_SIGHAND, CLONE_SYSVSEM, CLONE_VM — . , CLONE_FILES , ( fork()). — — , . . , CLONE_VM , , .


CLONE_THREAD : Linux — « », . , , getpid() , kill() — - , execve() — , .


, clone() fork(): , . clone() : , — . . ( , , .)


, pthread_create() , , . ?



fork() :


 pid_t child = fork(); if (child < 0) { /* fork() ,    */ } if (child == 0) { /*     execve() */ } /*     */ 

, . clone() . .


. , clone() , . syscall ret, , . .


SYSCALL + RET


, . , syscall ret:


 -if (region_size < 2) +if (region_size < 3) continue; /* * 0F 05 syscall + * C3 retq */ -for (size_t offset = 0; offset < region_size - 1; offset++) { +for (size_t offset = 0; offset < region_size - 2; offset++) { if (region_data[offset + 0] == 0x0F && - region_data[offset + 1] == 0x05) + region_data[offset + 1] == 0x05 && + region_data[offset + 2] == 0xC3) { return region->vaddr_low + offset; } } 

, .



. prepare_shellcode() , , :


  void write_shellcode(void) { char shellcode_text[SHELLCODE_TEXT_SIZE]; size_t shellcode_size = shellcode_end - shellcode_start; /*   ,  ,  . . */ prepare_shellcode(shellcode_text, shellcode_size); /*   -   */ write_remote_memory(target, shellcode_text_vaddr, shellcode_text, shellcode_size); + /*    «»   */ + unsigned long retaddr_vaddr = + shellcode_stack_vaddr + SHELLCODE_STACK_SIZE - 8; + write_remote_memory(target, retaddr_vaddr, + &shellcode_text_vaddr, sizeof(shellcode_text_vaddr)); } 

, , .


, , . System V ABI , ( %rsp) 16 . shellcode_stack_vaddr + SHELLCODE_STACK_SIZE : ( 4096 ), 1 . 8 , , retq, - . - :


 - sub $8,%rsp + sub $16,%rsp /*   */ mov %rsp,%rdi xor %rsi,%rsi mov %rax,%rdx xor %rcx,%rcx mov shellcode_address_pthread_create(%rip),%rax callq *%rax 

, %rsp 16 pthread_create(). SIGSEGV, — pthread_create() , .



, - , clone():


  static int spawn_shell_thread() { shell_tid = remote_clone(target, syscall_ret_vaddr, CLONE_FILES | CLONE_FS | CLONE_IO | CLONE_SIGHAND | CLONE_SYSVSEM | CLONE_THREAD | CLONE_VM, /*   **  */ - shellcode_stack_vaddr + SHELLCODE_STACK_SIZE); + shellcode_stack_vaddr + SHELLCODE_STACK_SIZE - 8); if (!shell_tid) return -1; return 0; } 

ptrace() SIGSTOP, :


 int ignore_thread_stop(pid_t pid) { return wait_for_process_stop(pid, SIGSTOP); } 

Eso es todo. ptrace():


 void resume_thread(pid_t pid) { ptrace(PTRACE_CONT, pid, 0, 0); } 


, , , exit(). waitpid(). — CLONE_THREAD wait() ,— PTRACE_O_TRACECLONE, :


 int wait_for_process_exit(pid_t pid) { int status = 0; if (waitpid(pid, &status, 0) < 0) return -1; if (!WIFEXITED(status)) return -1; return WEXITSTATUS(status); } 

pthread , , pthread_join() pthread , . , — . , , .


Memoria libre


, - . , - , munmap():


 void remote_munmap(pid_t pid, unsigned long syscall_insn_vaddr, unsigned long addr, size_t len) { perform_syscall(pid, syscall_insn_vaddr, __NR_munmap, 2, (long) addr, (long) len); } static void unmap_shellcode() { remote_munmap(target, syscall_ret_vaddr, shellcode_text_vaddr, SHELLCODE_TEXT_SIZE); remote_munmap(target, syscall_ret_vaddr, shellcode_stack_vaddr, SHELLCODE_STACK_SIZE); } 

, , , — ptrace() . (, SIGSTOP), , ( ):


 int stop_thread(pid_t pid) { if (kill(pid, SIGSTOP) < 0) return -1; if (wait_for_process_stop(pid, SIGSTOP) < 0) return -1; return 0; } 


, , . PTRACE_DETACH:


 int ptrace_detach(pid_t pid) { if (ptrace(PTRACE_DETACH, pid, 0, 0) < 0) return -1; return 0; } 


, . , . , .


Conclusión


? . , , .


demostración de inyección en el Centro de Control de Gnomos


Linux . GTK+ . , make:


 libpayload.so: payload.c $(CC) $(CFLAGS) $(shell pkg-config --cflags --libs gtk+-3.0) -shared -o $@ $< 

entry() GTK- — GTK UI , :


 #include <glib.h> #include <gtk/gtk.h> static gboolean actual_entry(gpointer _arg) { /*       : */ hook_gtk_entry_constructor(); /*   FALSE,       */ return FALSE; } void entry(void) { /*    -,   */ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, actual_entry, NULL, NULL); } 

, GTK , GtkEntry . "input-purpose" . «», , .


GTK glib — — GtkEntry . constructed(), . :


 static void (*old_gtk_entry_constructed)(GObject *object); static void new_gtk_entry_constructed(GObject *object) { GtkEntry *entry = GTK_ENTRY(object); /*    */ old_gtk_entry_constructed(object); /*    ,  ,   entry */ } static void hook_gtk_entry_constructor(void) { /*     GtkEntry */ GTypeClass *entry_type_class = g_type_class_peek(GTK_TYPE_ENTRY); GObjectClass *entry_object_class = G_OBJECT_CLASS(entry_type_class); /* *     "constructed"     . */ old_gtk_entry_constructed = entry_object_class->constructed; entry_object_class->constructed = new_gtk_entry_constructed; } 

GtkEntry :


  • , ,

, GtkEntry , , . , :


 static void new_gtk_entry_constructed(GObject *object) { GtkEntry *entry = GTK_ENTRY(object); old_gtk_entry_constructed(object); /*       */ g_signal_connect(entry, "notify::input-purpose", G_CALLBACK(input_purpose_changed), NULL); /*      */ g_signal_connect(entry, "icon-press", G_CALLBACK(icon_pressed), NULL); /*      */ g_signal_connect(entry, "icon-release", G_CALLBACK(icon_released), NULL); } 

. , . .


 static void input_purpose_changed(GtkEntry *entry) { GtkInputPurpose purpose = gtk_entry_get_input_purpose(entry); if (purpose == GTK_INPUT_PURPOSE_PASSWORD) { gtk_entry_set_icon_activatable(entry, GTK_ENTRY_ICON_PRIMARY, TRUE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-remove"); } else { gtk_entry_set_icon_activatable(entry, GTK_ENTRY_ICON_PRIMARY, FALSE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, NULL); } } 

: , , , - , :


 static void icon_pressed(GtkEntry *entry, GtkEntryIconPosition position) { if (position != GTK_ENTRY_ICON_PRIMARY) return; gtk_entry_set_visibility(entry, TRUE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-add"); } static void icon_released(GtkEntry *entry, GtkEntryIconPosition position) { if (position != GTK_ENTRY_ICON_PRIMARY) return; gtk_entry_set_visibility(entry, FALSE); gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_PRIMARY, "list-remove"); } 

Eso es todo


GitHub (GPLv2).


, . gdb :


 $ gdb --pid $(pgrep target) \ --batch \ -ex 'compile file -raw shell-code.c' 

Source: https://habr.com/ru/post/473740/


All Articles